Generate a Key Pair
IBM Digital Asset Haven supports ECDSA, EdDSA, and RSA asymmetric key algorithms for credentials. IBM Digital Asset Haven supports multiple curves. You can use any curve supported by Node.js’s crypto library.
IBM Digital Asset Haven recommends the following curves and modulus lengths.
| Algorithm | Curve/Length |
|---|---|
| ECDSA | secp256r1 (prime256v1 in OpenSSL) |
| EDDSA | Ed25519 |
| RSA | 3072 bits |
Examples
- OpenSSL CLI
-
# Recommended: Generate a EDDSA private key # NOTE: This is not the key for the blockchain, only for the API. # NOTE: EDDSA keys do not work in Postman. openssl genpkey -algorithm Ed25519 -out ed25519key.pem # Generate the public key openssl pkey -in ed25519key.pem -pubout -out ed25519key.pub.pem #OR # Generate a ECDSA private key # NOTE: This is not the key for the blockchain, only for the API. openssl ecparam -genkey -name prime256v1 -noout -out prime256v1.pem # Generate the public pey openssl pkey -in prime256v1.pem -pubout -out prime256v1.pub.pem #OR # Generate RSA private key openssl genrsa -out rsa3072.pem 3072 # Generate the public key openssl pkey -in rsa3072.pem -pubout -out rsa3072.pub.pemOn MacOS you may need to update your OpenSSL version to add support for EDDSA keys.brew update brew install openssl@1.1 echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> ~/.bash_profile - Node.js Crypto
-
import crypto from 'crypto' // EDDSA Key const eddsaKey = crypto.generateKeyPairSync('ed25519') const eddsaPublicKey: string = eddsaKey.publicKey.export({ type: 'spki', format: 'pem' }) const eddsaPrivateKey: string = eddsaKey.privateKey.export({ type: 'pkcs8', format: 'pem' }) // ECDSA Key const ecdsaKey: crypto.KeyPairKeyObjectResult = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) const ecdsaPublicKey: string = ecdsaKey.publicKey.export({ type: 'spki', format: 'pem' }) const ecdsaPrivateKey: string = ecdsaKey.privateKey.export({ type: 'pkcs8', format: 'pem' }) // RSA Key const rsaKey: crypto.KeyPairKeyObjectResult = crypto.generateKeyPairSync('rsa', { modulusLength: 3072 }) const rsaPublicKey: string = rsaKey.publicKey.export({ type: 'pkcs1', format: 'pem' }) const rsaPrivateKey: string = rsaKey.privateKey.export({ type: 'pkcs1', format: 'pem' }) - Web Crypto
-
// Code taken from https://github.com/mdn/dom-examples/blob/main/web-crypto/export-key/pkcs8.js function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } async function exportPrivateKey(key) { const exported = await window.crypto.subtle.exportKey('pkcs8', key) const exportedAsString = ab2str(exported) const exportedAsBase64 = window.btoa(exportedAsString) return `-----BEGIN PRIVATE KEY-----\n${exportedAsBase64}\n-----END PRIVATE KEY-----` } async function exportPublicKey(key, format) { const exported = await window.crypto.subtle.exportKey('spki', key) const exportedAsString = ab2str(exported) const exportedAsBase64 = window.btoa(exportedAsString) return `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----` } // EDDSA Key /* EDDSA support in Web Crypto is experimental and may not be present in all browsers See: https://nodejs.org/api/webcrypto.html#ed25519ed448x25519x448-key-pairs */ const eddsaKey = await window.crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']) const eddsaPublicKey = await exportPublicKey(eddsaKey) const eddsaPrivateKey = await exportPrivateKey(eddsaKey) // ECDSA Key const ecdsaKey = await window.crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']) const ecdsaPublicKey = await exportPublicKey(ecdsaKey) const ecdsaPrivateKey = await exportPrivateKey(ecdsaKey) // RSA Key const rsaKey = await window.crypto.subtle.generateKey( { name: 'RSA-PSS', modulusLength: 3072, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ['sign', 'verify'] ) const rsaPublicKey = await exportPublicKey(rsaKey) const rsaPrivateKey = await exportPrivateKey(rsaKey) - Python
-
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa # EDDSA Key (Ed25519) eddsa_private_key = ed25519.Ed25519PrivateKey.generate() eddsa_public_key_pem = eddsa_private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode() eddsa_private_key_pem = eddsa_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ).decode() # ECDSA Key (P-256) ecdsa_private_key = ec.generate_private_key(ec.SECP256R1()) ecdsa_public_key_pem = ecdsa_private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode() ecdsa_private_key_pem = ecdsa_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ).decode() # RSA Key (3072 bits) rsa_private_key = rsa.generate_private_key( public_exponent=65537, key_size=3072 ) rsa_public_key_pem = rsa_private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode() rsa_private_key_pem = rsa_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ).decode() - AWS CLI
-
# Generate a ECDSA Private Key aws kms create-key --key-spec ECC_NIST_P256 --key-usage SIGN_VERIFY # Get the Public Key aws kms get-public-key --key-id <KEY_ID> # Generate a RSA Private Key aws kms create-key --key-spec RSA_3072 --key-usage SIGN_VERIFY # Get the Public Key aws kms get-public-key --key-id <KEY_ID>
Signature format
An ECDSA (including EdDSA) signature consists of two values: r and s. Different cryptographic libraries encode these values differently. The two most common formats are ASN.1/DER and raw.
IBM Digital Asset Haven APIs expect ECDSA/EdDSA signatures in ASN.1/DER format.
- Raw signatures
- In raw format, the r and s values are concatenated directly to form the signature. Each must be exactly 32 bytes long. If a value is shorter, pad it with leading zeros.
- ASN.1/DER
- In ASN.1/DER format, values are encoded deterministically.
- The first byte (0x30) identifies the encoding.
- The second byte specifies the remaining length of the signature.
- The remaining bytes encode
rands. - Each value begins with 0x02 (separator), followed by its length, then the value itself as a minimal signed big-endian integer.
- Leading zeros are removed.
- If the first byte is negative (0x80–0xFF), prepend a zero.
- Final length can range from 1 to 33 bytes.
- Converting from raw to ASN.1/DER
- If your library produces raw signatures, convert them to ASN.1/DER before sending to IBM Digital Asset Haven.
- JavaScript
function minimizeBigInt(value) { if (value.length === 0) { return value } const minValue = [0, ...value] for (let i = 0; i < minValue.length; ++i) { if (minValue[i] === 0) { continue } if (minValue[i] > 0x7f) { return minValue.slice(i-1) } return minValue.slice(i) } return new Uint8Array([0]) } function rawSignatureToAns1(rawSignature) { if (rawSignature.length !== 64) { console.log(rawSignature.length) return new Uint8Array([0]) } const r = rawSignature.slice(0, 32) const s = rawSignature.slice(32) const minR = minimizeBigInt(r) const minS = minimizeBigInt(s) return new Uint8Array([ 0x30, minR.length + minS.length + 4, 0x02, minR.length, ...minR, 0x02, minS.length, ...minS ]) } - Python
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature def raw_signature_to_asn1(raw_signature: bytes) -> bytes: """Convert a raw 64-byte signature to ASN.1 DER format.""" if len(raw_signature) != 64: raise ValueError(f"Expected 64 bytes, got {len(raw_signature)}") r = int.from_bytes(raw_signature[:32], byteorder='big') s = int.from_bytes(raw_signature[32:], byteorder='big') # encode_dss_signature returns the ASN.1 DER encoded signature return encode_dss_signature(r, s)
- JavaScript
Base64 and Base64Url encoding
Base64 encodes binary data into printable characters. Base64Url is a URL-safe variant of Base64. It replaces unsafe characters and removes padding. IBM Digital Asset Haven uses Base64Url encoding in many places.
- In Node.js, Buffer supports Base64Url encoding.
- In Python, use the Base64 module.
- JavaScript
// Convert a string to a base64url string Buffer.from('somerandomvalue').toString('base64url') // Convert a base64url string to a string Buffer.from('c29tZXJhbmRvbXZhbHVl', 'base64url').toString() - Python
import base64 # Convert a string to a base64url string def to_base64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b'=').decode() # Convert a base64url string to bytes def from_base64url(data: str) -> bytes: # Add padding if needed padding = 4 - len(data) % 4 if padding != 4: data += '=' * padding return base64.urlsafe_b64decode(data) # Example usage to_base64url(b'somerandomvalue') # 'c29tZXJhbmRvbXZhbHVl' from_base64url('c29tZXJhbmRvbXZhbHVl').decode() # 'somerandomvalue'
For environments without Buffer support or where an older Buffer implementation is used, the code can be used to convert a value to a Base64Url encoded string.
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer)
return btoa(String.fromCharCode(...bytes))
}
function arrayBufferToBase64Url(buffer) {
return arrayBufferToBase64(buffer)
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_')
}
Warning: Do not double-encode signatures. A common mistake is passing a Base64 string to an encoding function such as
base64url(). These functions encode binary data, do not convert between formats.- Incorrect: double-encodes the signature
function arrayBufferToBase64Url(buffer) { return base64url(arrayBufferToBase64(buffer)) } - Correct: character replacement only
function arrayBufferToBase64Url(buffer) { return arrayBufferToBase64(buffer) .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_') }
Double-encoding causes
Invalid signature errors. To diagnose, decode your signature with:
echo "<signature>" | base64 -D
If the output looks like another Base64 string (for example, MEYCIQCDsx...) instead of binary data, it has been double-encoded.