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.pem

On 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 r and s.
  • 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)

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.