|
| 1 | +package actions |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "encoding/base64" |
| 7 | + "encoding/hex" |
| 8 | + "encoding/json" |
| 9 | + "fmt" |
| 10 | + "io" |
| 11 | + "net/http" |
| 12 | + |
| 13 | + "github.com/bsv-blockchain/go-sdk/auth/brc104" |
| 14 | + "github.com/bsv-blockchain/go-sdk/auth/certificates" |
| 15 | + ec "github.com/bsv-blockchain/go-sdk/primitives/ec" |
| 16 | + "github.com/bsv-blockchain/go-sdk/transaction" |
| 17 | + sdk "github.com/bsv-blockchain/go-sdk/wallet" |
| 18 | + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet/internal/mapping" |
| 19 | + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet/internal/utils" |
| 20 | + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk" |
| 21 | + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk/primitives" |
| 22 | + "github.com/go-softwarelab/common/pkg/to" |
| 23 | +) |
| 24 | + |
| 25 | +// ProtocolIssuanceRequest represents the certificate signing request sent to the certifier |
| 26 | +// as part of the issuance protocol. |
| 27 | +type ProtocolIssuanceRequest struct { |
| 28 | + Type string `json:"type"` |
| 29 | + Nonce string `json:"clientNonce"` |
| 30 | + Fields map[string]string `json:"fields"` |
| 31 | + MasterKeyring map[string]string `json:"masterKeyring"` |
| 32 | +} |
| 33 | + |
| 34 | +// ProtocolIssuanceResponse represents the response from the certifier containing the signed certificate |
| 35 | +// and server nonce for verification. |
| 36 | +type ProtocolIssuanceResponse struct { |
| 37 | + Protocol string `json:"protocol"` |
| 38 | + Certificate *Certificate `json:"certificate"` |
| 39 | + ServerNonce string `json:"serverNonce"` |
| 40 | + Timestamp string `json:"timestamp"` |
| 41 | + Version string `json:"version"` |
| 42 | +} |
| 43 | + |
| 44 | +// Certificate represents a certificate as returned by the certifier in the issuance protocol response. |
| 45 | +type Certificate struct { |
| 46 | + Type string `json:"type"` |
| 47 | + SerialNumber string `json:"serialNumber"` |
| 48 | + Subject string `json:"subject"` |
| 49 | + Certifier string `json:"certifier"` |
| 50 | + RevocationOutpoint string `json:"revocationOutpoint"` |
| 51 | + Fields map[string]string `json:"fields"` |
| 52 | + Signature string `json:"signature"` |
| 53 | +} |
| 54 | + |
| 55 | +// PrepareIssuanceActionDataParams contains parameters for preparing the certificate issuance request |
| 56 | +type PrepareIssuanceActionDataParams struct { |
| 57 | + Wallet sdk.Interface |
| 58 | + Args sdk.AcquireCertificateArgs |
| 59 | + Nonce []byte |
| 60 | + IdentityKey *ec.PublicKey |
| 61 | +} |
| 62 | + |
| 63 | +// PrepareIssuanceActionDataResult contains the prepared payload and related data |
| 64 | +type PrepareIssuanceActionDataResult struct { |
| 65 | + Body []byte |
| 66 | + Fields map[string]string |
| 67 | + MasterKeyring map[string]string |
| 68 | + CertTypeB64 string |
| 69 | + CounterParty sdk.Counterparty |
| 70 | +} |
| 71 | + |
| 72 | +// ParseCertificateResponseParams contains parameters for parsing the certificate response |
| 73 | +type ParseCertificateResponseParams struct { |
| 74 | + Response *http.Response |
| 75 | + Args sdk.AcquireCertificateArgs |
| 76 | + Nonce []byte |
| 77 | + IdentityKey *ec.PublicKey |
| 78 | +} |
| 79 | + |
| 80 | +// ParseCertificateResponseResult contains the parsed and validated certificate |
| 81 | +type ParseCertificateResponseResult struct { |
| 82 | + Certificate *certificates.Certificate |
| 83 | + ParsedCertifier *ec.PublicKey |
| 84 | + RevocationOutpoint *transaction.Outpoint |
| 85 | + ParsedSignature *ec.Signature |
| 86 | + SerialNumber []byte |
| 87 | + ServerNonce string |
| 88 | + CertFields map[sdk.CertificateFieldNameUnder50Bytes]sdk.StringBase64 |
| 89 | +} |
| 90 | + |
| 91 | +// StoreCertificateParams contains parameters for storing the certificate |
| 92 | +type StoreCertificateParams struct { |
| 93 | + Storage AcquireCertificateIssuanceStorage |
| 94 | + Auth wdk.AuthID |
| 95 | + Certificate *certificates.Certificate |
| 96 | + Certifier *ec.PublicKey |
| 97 | + RevocationOutpoint *transaction.Outpoint |
| 98 | + Signature *ec.Signature |
| 99 | + IdentityKey *ec.PublicKey |
| 100 | + CertTypeB64 string |
| 101 | + Fields map[string]string |
| 102 | + MasterKeyring map[string]string |
| 103 | +} |
| 104 | + |
| 105 | +// AcquireCertificateIssuanceStorage defines the storage methods needed for certificate issuance |
| 106 | +type AcquireCertificateIssuanceStorage interface { |
| 107 | + GetAuth(ctx context.Context) (wdk.AuthID, error) |
| 108 | + InsertCertificateAuth(ctx context.Context, cert *wdk.TableCertificateX) (uint, error) |
| 109 | +} |
| 110 | + |
| 111 | +// PrepareIssuanceActionData creates the certificate signing request payload along with certificate data |
| 112 | +func PrepareIssuanceActionData(ctx context.Context, p PrepareIssuanceActionDataParams) (*PrepareIssuanceActionDataResult, error) { |
| 113 | + // Prepare counterparty for encryption |
| 114 | + counterParty := sdk.Counterparty{ |
| 115 | + Counterparty: p.Args.Certifier, |
| 116 | + Type: sdk.CounterpartyTypeOther, |
| 117 | + } |
| 118 | + |
| 119 | + // Create encrypted certificate fields |
| 120 | + fieldsForEncryption, err := mapping.MapToFieldsForEncryption(p.Args.Fields) |
| 121 | + if err != nil { |
| 122 | + return nil, fmt.Errorf("failed to map fields to fields for encryption: %w", err) |
| 123 | + } |
| 124 | + |
| 125 | + certificateFieldsResult, err := certificates.CreateCertificateFields(ctx, p.Wallet, counterParty, fieldsForEncryption, to.Value(p.Args.Privileged), p.Args.PrivilegedReason) |
| 126 | + if err != nil { |
| 127 | + return nil, fmt.Errorf("failed to create certificate fields: %w", err) |
| 128 | + } |
| 129 | + |
| 130 | + fields := make(map[string]string, len(certificateFieldsResult.CertificateFields)) |
| 131 | + for k, v := range certificateFieldsResult.CertificateFields { |
| 132 | + fields[to.String(k)] = to.String(v) |
| 133 | + } |
| 134 | + |
| 135 | + masterKeyring := make(map[string]string, len(certificateFieldsResult.MasterKeyring)) |
| 136 | + for k, v := range certificateFieldsResult.MasterKeyring { |
| 137 | + masterKeyring[to.String(k)] = to.String(v) |
| 138 | + } |
| 139 | + |
| 140 | + // Build certificate signing request |
| 141 | + certTypeB64 := base64.StdEncoding.EncodeToString(p.Args.Type[:]) |
| 142 | + body, err := json.Marshal(&ProtocolIssuanceRequest{ |
| 143 | + Type: certTypeB64, |
| 144 | + Nonce: string(p.Nonce), |
| 145 | + Fields: fields, |
| 146 | + MasterKeyring: masterKeyring, |
| 147 | + }) |
| 148 | + if err != nil { |
| 149 | + return nil, fmt.Errorf("failed to marshal HTTP request payload: %w", err) |
| 150 | + } |
| 151 | + |
| 152 | + return &PrepareIssuanceActionDataResult{ |
| 153 | + Body: body, |
| 154 | + Fields: fields, |
| 155 | + MasterKeyring: masterKeyring, |
| 156 | + CertTypeB64: certTypeB64, |
| 157 | + CounterParty: counterParty, |
| 158 | + }, nil |
| 159 | +} |
| 160 | + |
| 161 | +// ParseCertificateResponse parses and validates the certificate response from the certifier |
| 162 | +func ParseCertificateResponse(p ParseCertificateResponseParams) (*ParseCertificateResponseResult, error) { |
| 163 | + // Read response body |
| 164 | + responseBytes, err := io.ReadAll(p.Response.Body) |
| 165 | + if err != nil { |
| 166 | + return nil, fmt.Errorf("failed to read response body: %w", err) |
| 167 | + } |
| 168 | + |
| 169 | + if p.Response.StatusCode < 200 || p.Response.StatusCode >= 300 { |
| 170 | + return nil, fmt.Errorf("unexpected HTTP status code %d: %s", p.Response.StatusCode, string(responseBytes)) |
| 171 | + } |
| 172 | + |
| 173 | + var response ProtocolIssuanceResponse |
| 174 | + if err := json.Unmarshal(responseBytes, &response); err != nil { |
| 175 | + return nil, fmt.Errorf("failed to unmarshal response: %w", err) |
| 176 | + } |
| 177 | + |
| 178 | + // Validate response headers and required fields |
| 179 | + responseAuthHeader := p.Response.Header.Get(brc104.HeaderIdentityKey) |
| 180 | + if responseAuthHeader != p.Args.Certifier.ToDERHex() { |
| 181 | + return nil, fmt.Errorf("invalid certifier! Expected: %s, Received: %s", p.Args.Certifier.ToDERHex(), responseAuthHeader) |
| 182 | + } |
| 183 | + |
| 184 | + if response.ServerNonce == "" { |
| 185 | + return nil, fmt.Errorf("no serverNonce received from certifier") |
| 186 | + } |
| 187 | + |
| 188 | + if response.Certificate == nil { |
| 189 | + return nil, fmt.Errorf("no certificate received from certifier") |
| 190 | + } |
| 191 | + |
| 192 | + // Parse certificate components |
| 193 | + subject, err := ec.PublicKeyFromString(response.Certificate.Subject) |
| 194 | + if err != nil { |
| 195 | + return nil, fmt.Errorf("failed to parse subject field to public key: %w", err) |
| 196 | + } |
| 197 | + |
| 198 | + certifier, err := ec.PublicKeyFromString(response.Certificate.Certifier) |
| 199 | + if err != nil { |
| 200 | + return nil, fmt.Errorf("failed to parse certifier field to public key: %w", err) |
| 201 | + } |
| 202 | + |
| 203 | + revocationOutpoint, err := transaction.OutpointFromString(response.Certificate.RevocationOutpoint) |
| 204 | + if err != nil { |
| 205 | + return nil, fmt.Errorf("failed to parse revocation outpoint: %w", err) |
| 206 | + } |
| 207 | + |
| 208 | + signatureBytes, err := hex.DecodeString(response.Certificate.Signature) |
| 209 | + if err != nil { |
| 210 | + return nil, fmt.Errorf("failed to decode signature from hex: %w", err) |
| 211 | + } |
| 212 | + |
| 213 | + parsedSignature, err := ec.ParseSignature(signatureBytes) |
| 214 | + if err != nil { |
| 215 | + return nil, fmt.Errorf("failed to parse signature: %w", err) |
| 216 | + } |
| 217 | + |
| 218 | + certFields, err := mapping.MapToCertificateFields(response.Certificate.Fields) |
| 219 | + if err != nil { |
| 220 | + return nil, fmt.Errorf("failed to map certificate fields: %w", err) |
| 221 | + } |
| 222 | + |
| 223 | + signedCert := certificates.NewCertificate( |
| 224 | + sdk.StringBase64(response.Certificate.Type), |
| 225 | + sdk.StringBase64(response.Certificate.SerialNumber), |
| 226 | + to.Value(subject), |
| 227 | + to.Value(certifier), |
| 228 | + revocationOutpoint, |
| 229 | + certFields, |
| 230 | + signatureBytes) |
| 231 | + |
| 232 | + serialNumber, err := base64.StdEncoding.DecodeString(string(signedCert.SerialNumber)) |
| 233 | + if err != nil { |
| 234 | + return nil, fmt.Errorf("failed to decode serialNumber: %w", err) |
| 235 | + } |
| 236 | + |
| 237 | + return &ParseCertificateResponseResult{ |
| 238 | + Certificate: signedCert, |
| 239 | + ParsedCertifier: certifier, |
| 240 | + RevocationOutpoint: revocationOutpoint, |
| 241 | + ParsedSignature: parsedSignature, |
| 242 | + SerialNumber: serialNumber, |
| 243 | + ServerNonce: response.ServerNonce, |
| 244 | + CertFields: certFields, |
| 245 | + }, nil |
| 246 | +} |
| 247 | + |
| 248 | +// VerifyCertificateIssuance verifies the certificate against the original request parameters |
| 249 | +func VerifyCertificateIssuance(ctx context.Context, wallet sdk.Interface, parsedCert *ParseCertificateResponseResult, nonce []byte, issuanceActionData *PrepareIssuanceActionDataResult, subject, certifier *ec.PublicKey, originator string) error { |
| 250 | + // Verify if serial number has correct length |
| 251 | + if len(parsedCert.SerialNumber) != utils.NonceHMACSize { |
| 252 | + return fmt.Errorf("invalid serialNumber length: got %d, want %d", len(parsedCert.SerialNumber), utils.NonceHMACSize) |
| 253 | + } |
| 254 | + // Verify HMAC of serial number |
| 255 | + dataToVerify := bytes.Join([][]byte{nonce, []byte(parsedCert.ServerNonce)}, []byte{}) |
| 256 | + var hmacToVerifyArray [32]byte |
| 257 | + copy(hmacToVerifyArray[:], parsedCert.SerialNumber) |
| 258 | + |
| 259 | + verifyHmacResult, err := wallet.VerifyHMAC(ctx, sdk.VerifyHMACArgs{ |
| 260 | + HMAC: hmacToVerifyArray, |
| 261 | + Data: dataToVerify, |
| 262 | + EncryptionArgs: sdk.EncryptionArgs{ |
| 263 | + KeyID: parsedCert.ServerNonce + string(nonce), |
| 264 | + ProtocolID: sdk.Protocol{ |
| 265 | + SecurityLevel: sdk.SecurityLevelEveryAppAndCounterparty, |
| 266 | + Protocol: "certificate issuance", |
| 267 | + }, |
| 268 | + Counterparty: issuanceActionData.CounterParty, |
| 269 | + }, |
| 270 | + }, originator) |
| 271 | + if err != nil { |
| 272 | + return fmt.Errorf("failed to verify HMAC signature: %w", err) |
| 273 | + } |
| 274 | + if !verifyHmacResult.Valid { |
| 275 | + return fmt.Errorf("invalid serialNumber") |
| 276 | + } |
| 277 | + |
| 278 | + // Validate certificate type |
| 279 | + if string(parsedCert.Certificate.Type) != issuanceActionData.CertTypeB64 { |
| 280 | + return fmt.Errorf("invalid certificate type! Expected: %s, Received: %s", issuanceActionData.CertTypeB64, parsedCert.Certificate.Type) |
| 281 | + } |
| 282 | + |
| 283 | + // Validate certificate subject matches our identity key |
| 284 | + if parsedCert.Certificate.Subject.ToDERHex() != subject.ToDERHex() { |
| 285 | + return fmt.Errorf("invalid certificate subject! Expected: %s, Received: %s", subject.ToDERHex(), parsedCert.Certificate.Subject.ToDERHex()) |
| 286 | + } |
| 287 | + |
| 288 | + // Validate certifier |
| 289 | + if parsedCert.Certificate.Certifier.ToDERHex() != certifier.ToDERHex() { |
| 290 | + return fmt.Errorf("invalid certifier! Expected: %s, Received: %s", certifier.ToDERHex(), parsedCert.Certificate.Certifier.ToDERHex()) |
| 291 | + } |
| 292 | + |
| 293 | + // Validate revocation outpoint exists |
| 294 | + if parsedCert.Certificate.RevocationOutpoint == nil { |
| 295 | + return fmt.Errorf("invalid revocationOutpoint") |
| 296 | + } |
| 297 | + |
| 298 | + // Validate that certificate fields match what we sent |
| 299 | + if len(parsedCert.Certificate.Fields) != len(issuanceActionData.Fields) { |
| 300 | + return fmt.Errorf("fields mismatch! Objects have different number of keys. Expected: %d, Received: %d", len(issuanceActionData.Fields), len(parsedCert.Certificate.Fields)) |
| 301 | + } |
| 302 | + |
| 303 | + for fieldName, fieldValue := range issuanceActionData.Fields { |
| 304 | + signedCertFieldValue, isPresent := parsedCert.Certificate.Fields[sdk.CertificateFieldNameUnder50Bytes(fieldName)] |
| 305 | + if !isPresent { |
| 306 | + return fmt.Errorf("missing field: %s in certificate fields from the certifier", fieldName) |
| 307 | + } |
| 308 | + |
| 309 | + if string(signedCertFieldValue) != fieldValue { |
| 310 | + return fmt.Errorf("invalid field! Expected: %s, Received: %s", fieldValue, string(signedCertFieldValue)) |
| 311 | + } |
| 312 | + } |
| 313 | + |
| 314 | + // Verify certificate signature |
| 315 | + if err := parsedCert.Certificate.Verify(ctx); err != nil { |
| 316 | + return fmt.Errorf("failed to verify certificate: %w", err) |
| 317 | + } |
| 318 | + |
| 319 | + return nil |
| 320 | +} |
| 321 | + |
| 322 | +// TestCertificateDecryption verifies that the certificate fields can be decrypted |
| 323 | +func TestCertificateDecryption(ctx context.Context, wallet sdk.Interface, certFields map[sdk.CertificateFieldNameUnder50Bytes]sdk.StringBase64, masterKeyring map[string]string, counterParty sdk.Counterparty, args sdk.AcquireCertificateArgs) error { |
| 324 | + certMasterKeyring, err := mapping.MapToCertificateFields(masterKeyring) |
| 325 | + if err != nil { |
| 326 | + return fmt.Errorf("failed to map certificate master keyring fields: %w", err) |
| 327 | + } |
| 328 | + |
| 329 | + _, err = certificates.DecryptFields(ctx, wallet, |
| 330 | + certMasterKeyring, |
| 331 | + certFields, |
| 332 | + counterParty, |
| 333 | + to.Value(args.Privileged), |
| 334 | + args.PrivilegedReason, |
| 335 | + ) |
| 336 | + if err != nil { |
| 337 | + return fmt.Errorf("failed to decrypt certificate: %w", err) |
| 338 | + } |
| 339 | + |
| 340 | + return nil |
| 341 | +} |
| 342 | + |
| 343 | +// StoreCertificate saves the certificate to storage |
| 344 | +func StoreCertificate(ctx context.Context, p StoreCertificateParams) error { |
| 345 | + // Convert signature to hex string for storage |
| 346 | + rHex := fmt.Sprintf("%064x", p.Signature.R) |
| 347 | + sHex := fmt.Sprintf("%064x", p.Signature.S) |
| 348 | + sigHex := rHex + sHex |
| 349 | + |
| 350 | + // Parse fields into TableCertificateField slice |
| 351 | + certificateFields, err := wdk.ParseToTableCertificateFieldSlice(to.Value(p.Auth.UserID), p.Fields, p.MasterKeyring) |
| 352 | + if err != nil { |
| 353 | + return fmt.Errorf("failed to parse certificate fields for user %d: %w", to.Value(p.Auth.UserID), err) |
| 354 | + } |
| 355 | + |
| 356 | + // Insert certificate into storage |
| 357 | + certifierPubKeyHex := primitives.PubKeyHex(p.Certifier.ToDERHex()) |
| 358 | + _, err = p.Storage.InsertCertificateAuth(ctx, &wdk.TableCertificateX{ |
| 359 | + TableCertificate: wdk.TableCertificate{ |
| 360 | + UserID: to.Value(p.Auth.UserID), |
| 361 | + Type: primitives.Base64String(p.CertTypeB64), |
| 362 | + SerialNumber: primitives.Base64String(p.Certificate.SerialNumber), |
| 363 | + Certifier: certifierPubKeyHex, |
| 364 | + Subject: primitives.PubKeyHex(p.IdentityKey.ToDERHex()), |
| 365 | + RevocationOutpoint: primitives.OutpointString(p.RevocationOutpoint.String()), |
| 366 | + Signature: primitives.HexString(sigHex), |
| 367 | + Verifier: to.Ptr(certifierPubKeyHex), // Certifier is the verifier (KeyringRevealer.Certifier is true) |
| 368 | + }, |
| 369 | + Fields: certificateFields, |
| 370 | + }) |
| 371 | + if err != nil { |
| 372 | + return fmt.Errorf("failed to insert certificate for user %d: %w", to.Value(p.Auth.UserID), err) |
| 373 | + } |
| 374 | + |
| 375 | + return nil |
| 376 | +} |
0 commit comments