Anchorage Digital Addresses API
These endpoints allow the user to create and retrieve deposit addresses for specific assets. # Verifying Deposit Addresses The addresses REST API endpoints return signatures of the address strings and other metadata that prove the address was generated by Anchorage Digital for your organization. It is critical that clients verify the address signature and any accompanying metadata before the address is used, to confirm the address authenticity and integrity. ## Address Signature Schemes The API supports two address verification schemes: - **V1 Address Signatures** - The original scheme involves verifying a signature against a public key that is unique to each organization. This public key remains fixed for the lifetime of the organization and is distributed on request by Anchorage Digital out-of-band. While the public key does not need to be kept confidential, it must be kept tamper-proof. - **V2 Address Signatures** - The newer scheme involves verifying a signature against the public key of the leaf certificate of a X509 certificate chain returned alongside the signature, and verifying the certificate chain itself against the Anchorage Digital Address Signing Root CA, provided below, which must be hard-coded by API clients. You can determine which scheme an address uses by checking the `signatureVersion` field. ## V1 Address Signature Verification The steps for verifying V1 address signatures are as follows: 1. Check the validity of the signature: a. Decode the `addressSignaturePayload` field from hex to bytes. b. Decode the `signature` field from hex to bytes. c. Using the fixed public key for this organization, verify that `signatureBytes` is a valid Ed25519 signature of the `addressSignaturePayloadBytes`. 2. Verify the signed address matches the address to be used: a. Decode the `addressSignaturePayload` field from hex to bytes. b. Parse the bytes as a JSON object. c. Verify the address to be used matches the value of the `TextAddress` property from the JSON object. **Note: It is not sufficient to validate the signature without also validating that the address contained in the JSON decoded from the `addressSignaturePayload` matches the address to be used.** ### V1 Signed Payload Fields - `TextAddress` — The text format of the on-chain address. ```json { "TextAddress": "2N19AcihQ1a4MxQW658UFHTioUNnMkiHPkw" } ``` ### Sample V1 validation code: ```go package main import ( "crypto/ed25519" "encoding/hex" "encoding/json" "fmt" ) // V1SignedPayload represents the JSON structure in the addressSignaturePayload for V1 signatures type V1SignedPayload struct { TextAddress string `json:"TextAddress"` } // verifyV1AddressSignature verifies a V1 address signature. // // Parameters: // - address: The address string from the API response // - addressSignaturePayload: Hex-encoded bytes that were signed // - signature: Hex-encoded Ed25519 signature // - orgPublicKeyHex: Hex-encoded Ed25519 public key for your organization (obtained out-of-band) // // Returns an error if verification fails. func verifyV1AddressSignature(address, addressSignaturePayload, signature, orgPublicKeyHex string) error { // Step 1: Check the validity of the signature // Decode the addressSignaturePayload from hex to bytes payloadBytes, err := hex.DecodeString(addressSignaturePayload) if err != nil { return fmt.Errorf("failed to decode addressSignaturePayload: %w", err) } // Decode the signature from hex to bytes signatureBytes, err := hex.DecodeString(signature) if err != nil { return fmt.Errorf("failed to decode signature: %w", err) } // Decode the organization public key from hex publicKeyBytes, err := hex.DecodeString(orgPublicKeyHex) if err != nil { return fmt.Errorf("failed to decode organization public key: %w", err) } if len(publicKeyBytes) != ed25519.PublicKeySize { return fmt.Errorf("invalid public key size: got %d bytes, expected %d", len(publicKeyBytes), ed25519.PublicKeySize) } publicKey := ed25519.PublicKey(publicKeyBytes) // Verify the Ed25519 signature if !ed25519.Verify(publicKey, payloadBytes, signatureBytes) { return fmt.Errorf("signature verification failed") } // Step 2: Verify the signed address matches the address to be used // Parse the payload bytes as JSON var signedPayload V1SignedPayload if err := json.Unmarshal(payloadBytes, &signedPayload); err != nil { return fmt.Errorf("failed to parse signed payload: %w", err) } // Verify the TextAddress matches if signedPayload.TextAddress != address { return fmt.Errorf("signed TextAddress does not match: signed=%q, expected=%q", signedPayload.TextAddress, address) } return nil } func main() { // Sample API response data address := "2N19AcihQ1a4MxQW658UFHTioUNnMkiHPkw" addressSignaturePayload := "7b225465787441646472657373223a22324e313941636968513161344d78515736353855464854696f554e6e4d6b6948506b77227d" signature := "b18f6848dc0fef01a069e7ac26046383bf5cd130203994dc2d72b5a9097351b1e8b67115b63124fbc8c16673566416a635913c670b676089339c62a7824baa03" // Organization public key - obtained out-of-band from Anchorage Digital beforehand // Unique per Organization, fixed for the lifetime of that Organization // Must be kept tamper-proof orgPublicKeyHex := "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c" // Verify the signature if err := verifyV1AddressSignature(address, addressSignaturePayload, signature, orgPublicKeyHex); err != nil { fmt.Printf("✗ V1 Address signature verification failed: %v\n", err) return } fmt.Println("✓ V1 Address signature verified successfully!") fmt.Printf(" Address: %s\n", address) fmt.Println("\nYou may now safely use this address for deposits.") } ``` ## V2 Address Signature Verification Addresses returned with `signatureVersion` set to `V2` also include a `certChain` field containing an x509 certificate chain in PEM format. The steps for verifying V2 address signatures are as follows: 1. Verify the certificate chain: a. Parse the `certChain` field as PEM-encoded x509 certificates **Note: The leaf certificate is at the 0th index, followed by zero or more intermediate certificates. The root cert is excluded from this response. The number of certificates in the chain is subject to change.** b. Verify the certificate chain from the leaf to the trusted Anchorage Digital Root CA. c. Verify all certificates are valid at the current time (both notAfter and notBefore). d. Verify the leaf certificate's Subject Alternative Names include `address-provider.anchorage.internal`. e. Verify the leaf certificate's KeyUsage includes both `digitalSignature` and `nonRepudiation` (also known as `contentCommitment`). f. Extract the public key from the leaf certificate. Currently, we only support ed25519 keys, however this is subject to change the future. 2. Verify the signature: a. Decode the `addressSignaturePayload` field from hex to bytes b. Decode the `signature` field from hex to bytes c. Using the public key from the leaf certificate, verify that `signatureBytes` is a valid signature of the `addressSignaturePayload` bytes. 3. Verify the signed details: a. Parse the `addressSignaturePayload` bytes as a JSON object b. Verify `SignatureExpiresAt` is greater or equal to the current UTC Unix Timestamp c. Verify `TextAddress` matches the address to be used d. Verify `VaultId` matches your expected Vault ID e. Verify `NetworkId` matches the expected network for this address **Note: The signed payload also includes a `NetworkName` field for human-readable purposes, which does not need to be verified.** **Note: API clients must not use "strict" JSON parsers which will disallow extra properties, as future versions may introduce additional fields.** **Note: Anchorage Digital will periodically refresh V2 signatures and our Address Signing Root CA before expiration. The deposit address itself will not change. Only the signature, certificate chain and Root CA will be updated.** ### V2 Signed Payload Fields - `TextAddress` — The text format of the on-chain address. - `VaultId` — Identifies the vault this address belongs to. - `NetworkId` — Identifies the network that this address can receive deposits on. - `NetworkName` — A human readable version of the `NetworkId`. - `SignatureExpiresAt` — The time after which the signature should not be trusted. ```json { "VaultId": "dae6089e7c0836705f0562af0f1e4e1f", "TextAddress": "bcrt1q709skemgf5skpsnysvgme2s3ztehkutl390yl0wp29lnmum5uw7qg0qrwm", "NetworkName": "Bitcoin Regnet", "NetworkId": "BTC_R", "SignatureExpiresAt": 1769450713 } ``` ### Anchorage Digital Address Signing Root CAs Clients are encouraged to hard-code the appropriate Root CA value for the environment they are making API requests against. It is essential that this value be tamper-proof. - Production Environment: ``` -----BEGIN CERTIFICATE----- MIIBXTCCAQ+gAwIBAgIUQZI+MSvYTXQHra+3OAKnwAMzotUwBQYDK2VwMCAxHjAcBgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAeFw0yNjAxMjYwMDAwMDBaFw0yNzAxMjYwMDAwMDBaMCAxHjAcBgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAqMAUGAytlcAMhADTh1nctgIHtAKNW8ww/bY606pJ3OP2dyZYcQrU2kG5jo1swWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAUBgorBgEEAYaNHwEBBAYWBHJvb3QwIAYDVR0RBBkwF4IVY2EuYW5jaG9yYWdlLmludGVybmFsMAUGAytlcANBANkkdudEjH9RTKbRAxrRXyMSS/TgmdSrAVYOZzoRDJlyc+5oD+a0pmmwWVe86xZi37YbN1GzVlXcJAPpV6ceEQU= -----END CERTIFICATE----- ``` - Staging Environment: ``` -----BEGIN CERTIFICATE----- MIIBXDCCAQ6gAwIBAgITOfTQ4rYUsghgvdl8YCJSC67uGDAFBgMrZXAwIDEeMBwGA1UEAwwVY2EuYW5jaG9yYWdlLmludGVybmFsMB4XDTI2MDEyNDAwMDAwMFoXDTI3MDEyNDAwMDAwMFowIDEeMBwGA1UEAwwVY2EuYW5jaG9yYWdlLmludGVybmFsMCowBQYDK2VwAyEAPlBo2/+kPPL0WRpT+B/yHsU25AN/M6HP2bzC61yHb4ajWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMBQGCisGAQQBho0fAQEEBhYEcm9vdDAgBgNVHREEGTAXghVjYS5hbmNob3JhZ2UuaW50ZXJuYWwwBQYDK2VwA0EAYsJxVI9n42liCF9f+Ou7uuC1QGFwaHwFsfOm0WFofSlE1trWqzj4ruzjPYSRJc8Ht2A7XCAfXkG0mzKpL/wQDg== -----END CERTIFICATE----- ``` ### Sample V2 validation code: ```go package main import ( "crypto/ed25519" "crypto/x509" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "time" ) // V2SignedPayload represents the JSON structure in the addressSignaturePayload for V2 signatures type V2SignedPayload struct { TextAddress string `json:"TextAddress"` VaultId string `json:"VaultId"` NetworkId string `json:"NetworkId"` NetworkName string `json:"NetworkName"` SignatureExpiresAt int64 `json:"SignatureExpiresAt"` // Unix timestamp } // verifyV2AddressSignature verifies a V2 address signature. // // Parameters: // - now: The "current" time. Note that conforming implementations must use // a trusted source for the current time. // - address: The address string from the API response // - addressSignaturePayload: Hex-encoded bytes that were signed // - signature: Hex-encoded signature // - certChainPEM: PEM-encoded certificate chain (leaf first, then intermediates) // - rootCAPEM: PEM-encoded Root CA certificate (hard-coded by client) // - expectedVaultId: Your Vault ID to verify against the signed VaultId // - expectedNetworkId: Expected network ID for this address (e.g., "BTC", "ETH") // // Returns an error if verification fails. func verifyV2AddressSignature( now time.Time, address, addressSignaturePayload, signature, certChainPEM, rootCAPEM, expectedVaultId, expectedNetworkId string, ) error { // Step 1: Verify the certificate chain // Parse the certificate chain from PEM certs, err := parsePEMCertificates([]byte(certChainPEM)) if err != nil { return fmt.Errorf("failed to parse certificate chain: %w", err) } if len(certs) == 0 { return fmt.Errorf("certificate chain is empty") } leafCert := certs[0] var intermediateCerts []*x509.Certificate if len(certs) > 1 { intermediateCerts = certs[1:] } // Parse the Root CA rootCACerts, err := parsePEMCertificates([]byte(rootCAPEM)) if err != nil { return fmt.Errorf("failed to parse Root CA: %w", err) } if len(rootCACerts) != 1 { return fmt.Errorf("expected exactly one Root CA certificate, got %d", len(rootCACerts)) } rootCA := rootCACerts[0] // Verify the leaf certificate's KeyUsage includes both // digitalSignature and nonRepudiation (AKA contentCommitment) if leafCert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { return fmt.Errorf("leaf certificate KeyUsage missing DigitalSignature") } if leafCert.KeyUsage&x509.KeyUsageContentCommitment == 0 { return fmt.Errorf("leaf certificate KeyUsage missing NonRepudiation (ContentCommitment)") } // Verify the certificate chain from leaf to Root CA roots := x509.NewCertPool() roots.AddCert(rootCA) intermediates := x509.NewCertPool() for _, cert := range intermediateCerts { intermediates.AddCert(cert) } // NOTE: Not all x509 libraries are created equal and are not // guaranteed to verify exactly the same things! // // Always review the library you plan to use and ensure it covers the // checks described in the User Guide! // // For example, the Go implementation checks all Certificates for // temporal validity (notBefore and notAfter against CurrentTime), for // valid signatures up the chain, and checks that the Subject // Alternative Names include the values in DNSNames below. // // However it does not check the KeyUsage bits, hence the additional // checks above. opts := x509.VerifyOptions{ DNSNames: []string{"address-provider.anchorage.internal"}, Roots: roots, Intermediates: intermediates, CurrentTime: now, // NOTE: This allows for any Extended Key Usage, but does not // check the Key Usage bits, hence the additional checks above. KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, } if _, err := leafCert.Verify(opts); err != nil { return fmt.Errorf("certificate chain verification failed: %w", err) } // Extract the public key from the leaf certificate leafPublicKey, ok := leafCert.PublicKey.(ed25519.PublicKey) if !ok { return fmt.Errorf("leaf certificate does not use Ed25519 (got type %T)", leafCert.PublicKey) } // Step 2: Verify the signature // Decode the addressSignaturePayload from hex to bytes payloadBytes, err := hex.DecodeString(addressSignaturePayload) if err != nil { return fmt.Errorf("failed to decode addressSignaturePayload: %w", err) } // Decode the signature from hex to bytes signatureBytes, err := hex.DecodeString(signature) if err != nil { return fmt.Errorf("failed to decode signature: %w", err) } // Verify the signature using the leaf certificate's public key if !ed25519.Verify(leafPublicKey, payloadBytes, signatureBytes) { return fmt.Errorf("signature verification failed") } // Step 3: Verify the signed details // Parse the payload bytes as JSON var signedPayload V2SignedPayload if err := json.Unmarshal(payloadBytes, &signedPayload); err != nil { return fmt.Errorf("failed to parse signed payload: %w", err) } // Verify SignatureExpiresAt is not in the past if now.Unix() > signedPayload.SignatureExpiresAt { expiryTime := time.Unix(signedPayload.SignatureExpiresAt, 0) return fmt.Errorf("signature has expired at %s", expiryTime) } // Verify TextAddress matches if signedPayload.TextAddress != address { return fmt.Errorf("signed TextAddress does not match: signed=%q, expected=%q", signedPayload.TextAddress, address) } // Verify VaultId matches if signedPayload.VaultId != expectedVaultId { return fmt.Errorf("signed VaultId does not match: signed=%q, expected=%q", signedPayload.VaultId, expectedVaultId) } // Verify NetworkId matches if signedPayload.NetworkId != expectedNetworkId { return fmt.Errorf("signed NetworkId does not match: signed=%q, expected=%q", signedPayload.NetworkId, expectedNetworkId) } return nil } // parsePEMCertificates parses PEM-encoded certificates and returns them as a slice func parsePEMCertificates(pemData []byte) ([]*x509.Certificate, error) { var certs []*x509.Certificate for { block, rest := pem.Decode(pemData) if block == nil { break } if block.Type != "CERTIFICATE" { pemData = rest continue } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } certs = append(certs, cert) pemData = rest } return certs, nil } func main() { // Sample API response data address := "bcrt1q709skemgf5skpsnysvgme2s3ztehkutl390yl0wp29lnmum5uw7qg0qrwm" addressSignaturePayload := "7b225661756c744964223a226461653630383965376330383336373035663035363261663066316534653166222c225465787441646472657373223a22626372743171373039736b656d676635736b70736e797376676d653273337a7465686b75746c333930796c30777032396c6e6d756d357577377167307172776d222c224e6574776f726b4e616d65223a22426974636f696e205265676e6574222c224e6574776f726b4964223a224254435f52222c225369676e6174757265457870697265734174223a313736393435303731337d" signature := "951eb2fb560e660aa9c3d1ccd120d3ad1a19d90d8747347057e48bf174330eb386089e3232d822fd66b8183cce8059c91183afde299b920a0e0c05c5b167360e" certChainPEM := `-----BEGIN CERTIFICATE----- MIIBYTCCAROgAwIBAgIUMLKt+K9eFku+P7BbefE1xAHg0hcwBQYDK2VwMAAwHhcNMjYwMTI2MTcwNDEzWhcNMjcwMTI2MTcwNTEzWjAuMSwwKgYDVQQDEyNhZGRyZXNzLXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAqMAUGAytlcAMhAPsgM70aWFYsZaLHawtYJpl42BkiTLyCq96+OXe4FxrVo3EwbzAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBS0usSFeB2gjC+wcowtxN3MeKSH7zAuBgNVHREEJzAlgiNhZGRyZXNzLXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMrZXADQQCXmvIkuPnUgCHxWmFmzvgWdv9lUlt84oZCel+OeJW9n8PR88tGxAcD1E3+KDBXVpO0GcRA0W9+xqqICAo2ROEJ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBKDCB26ADAgECAhRGsD05KldIse+uIEa976AijTqlxjAFBgMrZXAwADAeFw0yNjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQCNpyY5Sr21FHNvvLkBKG8AEMKdhqtajmV5d2QaZlmtAqNnMGUwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLS6xIV4HaCML7ByjC3E3cx4pIfvMCAGA1UdEQEB/wQWMBSCEmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMrZXADQQCIgw6kLMIwhd3ACjG03cJ5z/ZZp8aXXycFq2ZC9TLhieJ3rncyMH6ZdyJ3Ai1eVaHs4vnDCv54Vdh83vvSky4K -----END CERTIFICATE----- ` // NOTE: This is a FAKE Root CA used just for this example. // NOTE: Conforming client implementations should hard-code the real // Anchorage Digital Address Signing Root CA for the environment they // are making requests to. rootCAPEM := `-----BEGIN CERTIFICATE----- MIIBGzCBzqADAgECAhQ2qQwArneTuF0dbNDs8i/ExuyW2DAFBgMrZXAwADAeFw0yNjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQB+gEnytXKnuAMonIWGWnB0qyTqa0aw3l9u5VRbu86UgaNaMFgwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOj64tL1teJHkojsiblnnK34Tw+EMBYGA1UdEQEB/wQMMAqCCGludGVybmFsMAUGAytlcANBAAg2IcVEXmWKSivhUNSatNfMmASxi83QscIuyP/sIW2sRIuCqQJoo9lN6TaxzyV62cQMzthFOCZcgRE+k0JV7Ao= -----END CERTIFICATE----- ` // Your Vault ID - obtained from your application context expectedVaultId := "dae6089e7c0836705f0562af0f1e4e1f" // Expected network ID for this address expectedNetworkId := "BTC_R" // Implementations should use the actual current time // now := time.Now() now := time.Unix(1769450600, 0) // Fake time so that this example passes. // Verify the signature if err := verifyV2AddressSignature( now, address, addressSignaturePayload, signature, certChainPEM, rootCAPEM, expectedVaultId, expectedNetworkId, ); err != nil { fmt.Printf("✗ V2 Address signature verification failed: %v\n", err) return } fmt.Println("✓ V2 Address signature verified successfully!") fmt.Printf(" Address: %s\n", address) fmt.Printf(" Vault ID: %s\n", expectedVaultId) fmt.Printf(" Network: %s\n", expectedNetworkId) fmt.Println("\nYou may now safely use this address for deposits.") } ```