this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

atproto/crypto: in-repo SDK for atproto cryptography

+932
+13
atproto/crypto/docs.go
··· 1 + // Cryptographic keys and operations as used in atproto 2 + // 3 + // This package attempts to abstract away the specific curves, compressions, signature variations, and other implementation details. The goal is to provide as few knobs and options as possible when working with this library. Use of cryptography in atproto is specified in https://atproto.com/specs/cryptography. 4 + // 5 + // The two currently supported curve types are: 6 + // 7 + // - P-256/secp256r1, internally implemented using golang's stdlib cryptographic library 8 + // - K-256/secp256r1, internally implemented using <gitlab.com/yawning/secp256k1-voi> 9 + // 10 + // "Low-S" signatures are enforced for both key types, both when creating signatures and during verification, as required by the atproto specification. 11 + // 12 + // This package uses concrete types for private keys, meaning that the secret key material is present in memory. 13 + package crypto
+47
atproto/crypto/examples_test.go
··· 1 + package crypto 2 + 3 + import ( 4 + "encoding/base64" 5 + "fmt" 6 + ) 7 + 8 + func ExamplePublicKey() { 9 + pub, err := ParsePublicDidKey("did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo") 10 + if err != nil { 11 + panic("failed to parse did:key") 12 + } 13 + fmt.Println(pub.DidDocSuite()) 14 + 15 + // parse existing base64 message and signature to raw bytes 16 + msg, _ := base64.RawStdEncoding.DecodeString("oWVoZWxsb2V3b3JsZA") 17 + sig, _ := base64.RawStdEncoding.DecodeString("2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg") 18 + if err = pub.HashAndVerify(msg, sig); err != nil { 19 + fmt.Println("Verification Failed") 20 + } else { 21 + fmt.Println("Success!") 22 + } 23 + // Output: 24 + // EcdsaSecp256r1VerificationKey2019 25 + // Success! 26 + } 27 + 28 + func ExamplePrivateKey() { 29 + // create secure private key, and corresponding public key 30 + priv, err := GeneratePrivateKey(K256) 31 + if err != nil { 32 + panic("failed to generate key") 33 + } 34 + pub := priv.Public() 35 + 36 + // sign a message 37 + msg := []byte("hello world") 38 + sig, _ := priv.HashAndSign(msg) 39 + 40 + // verify the message 41 + if err = pub.HashAndVerify(msg, sig); err != nil { 42 + fmt.Println("Verification Failed") 43 + } else { 44 + fmt.Println("Success!") 45 + } 46 + // Output: Success! 47 + }
+91
atproto/crypto/interop_fixtures_test.go
··· 1 + package crypto 2 + 3 + import ( 4 + "encoding/base64" 5 + "encoding/json" 6 + "io" 7 + "os" 8 + "testing" 9 + 10 + "github.com/stretchr/testify/assert" 11 + ) 12 + 13 + type InteropFixture struct { 14 + MessageBase64 string `json:"messageBase64"` 15 + Algorithm string `json:"algorithm"` 16 + DidDocSuite string `json:"didDocSuite"` 17 + PublicKeyDid string `json:"publicKeyDid"` 18 + PublicKeyMultibase string `json:"publicKeyMultibase"` 19 + SignatureBase64 string `json:"signatureBase64"` 20 + ValidSignature bool `json:"validSignature"` 21 + } 22 + 23 + func TestInteropSignatureFixtures(t *testing.T) { 24 + // "p256" == "secp256r1" == "ES256" == "EcdsaSecp256r1VerificationKey2019" 25 + // "k256" == "secp256k1" == "ES256K" == "EcdsaSecp256k1VerificationKey2019" 26 + 27 + f, err := os.Open("testdata/signature-fixtures.json") 28 + if err != nil { 29 + t.Fatal(err) 30 + } 31 + defer f.Close() 32 + 33 + fixBytes, err := io.ReadAll(f) 34 + if err != nil { 35 + t.Fatal(err) 36 + } 37 + 38 + var fixtures []InteropFixture 39 + if err := json.Unmarshal(fixBytes, &fixtures); err != nil { 40 + t.Fatal(err) 41 + } 42 + 43 + for _, row := range fixtures { 44 + _ = row 45 + testSignatureFixture(t, row) 46 + } 47 + } 48 + 49 + func testSignatureFixture(t *testing.T, row InteropFixture) { 50 + assert := assert.New(t) 51 + 52 + var kt KeyType 53 + switch row.DidDocSuite { 54 + case "EcdsaSecp256r1VerificationKey2019": 55 + kt = P256 56 + case "EcdsaSecp256k1VerificationKey2019": 57 + kt = K256 58 + default: 59 + t.Fatal("expected DidDocSuite") 60 + } 61 + 62 + // parse all the fields 63 + pkDid, err := ParsePublicDidKey(row.PublicKeyDid) 64 + assert.NoError(err) 65 + pkCompMultibase, err := ParsePublicCompressedMultibase(row.PublicKeyMultibase, kt) 66 + assert.NoError(err) 67 + msgBytes, err := base64.RawStdEncoding.DecodeString(row.MessageBase64) 68 + assert.NoError(err) 69 + sigBytes, err := base64.RawStdEncoding.DecodeString(row.SignatureBase64) 70 + assert.NoError(err) 71 + 72 + // verify encodings 73 + assert.Equal(pkDid, pkCompMultibase, "key equality") 74 + assert.Equal(row.DidDocSuite, pkDid.DidDocSuite()) 75 + assert.Equal(row.DidDocSuite, pkCompMultibase.DidDocSuite()) 76 + assert.Equal(row.PublicKeyDid, pkDid.DidKey(), "did:key re-encoding") 77 + assert.Equal(row.PublicKeyMultibase, pkCompMultibase.CompressedMultibase(), "multibase re-encoding") 78 + 79 + // verify signatures 80 + if row.ValidSignature { 81 + assert.NoError(pkDid.HashAndVerify(msgBytes, sigBytes), "keyType=%v format=%v", row.Algorithm, "did:key") 82 + assert.NoError(pkCompMultibase.HashAndVerify(msgBytes, sigBytes), "keyType=%v format=%v", row.Algorithm, "multibase") 83 + } else { 84 + assert.Error(pkDid.HashAndVerify(msgBytes, sigBytes), "keyType=%v format=%v", row.Algorithm, "did:key") 85 + assert.Error(pkCompMultibase.HashAndVerify(msgBytes, sigBytes), "keyType=%v format=%v", row.Algorithm, "multibase") 86 + } 87 + 88 + // signatures don't match random data 89 + assert.Error(pkCompMultibase.HashAndVerify(msgBytes, []byte{1, 2, 3}), "keyType=%v format=%v", row.Algorithm, "multibase") 90 + assert.Error(pkCompMultibase.HashAndVerify([]byte{1, 2, 3}, sigBytes), "keyType=%v format=%v", row.Algorithm, "multibase") 91 + }
+462
atproto/crypto/keys.go
··· 1 + package crypto 2 + 3 + import ( 4 + "crypto" 5 + "crypto/ecdh" 6 + "crypto/ecdsa" 7 + "crypto/elliptic" 8 + "crypto/rand" 9 + "crypto/sha256" 10 + "crypto/x509" 11 + "fmt" 12 + "math/big" 13 + "strings" 14 + 15 + "github.com/mr-tron/base58" 16 + secp256k1 "gitlab.com/yawning/secp256k1-voi" 17 + secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec" 18 + ) 19 + 20 + // Represents the specific support curve type. It is not possible to use [elliptic.Curve] for this because some curves are not in stdlib 21 + type KeyType uint8 22 + 23 + const ( 24 + P256 KeyType = 1 // P-256 / secp256r1 / ES256 25 + K256 KeyType = 2 // K-256 / secp256k1 / ES256K 26 + ) 27 + 28 + type PrivateKey struct { 29 + keyType KeyType 30 + privP256 *ecdsa.PrivateKey 31 + privK256 *secp256k1secec.PrivateKey 32 + } 33 + 34 + type PublicKey struct { 35 + keyType KeyType 36 + pubP256 *ecdsa.PublicKey 37 + pubK256 *secp256k1secec.PublicKey 38 + } 39 + 40 + var k256Options = &secp256k1secec.ECDSAOptions{ 41 + // Used to *verify* digest, not to re-hash 42 + Hash: crypto.SHA256, 43 + // Use `[R | S]` encoding. 44 + Encoding: secp256k1secec.EncodingCompact, 45 + // Checking `s <= n/2` to prevent signature mallability is not part of SEC 1, Version 2.0. libsecp256k1 which used to be used by this package, includes the check, so retain behavior compatibility. 46 + RejectMalleable: true, 47 + } 48 + 49 + // Creates a secure new cryptographic key from scratch, with the indicated curve type. 50 + func GeneratePrivateKey(kt KeyType) (*PrivateKey, error) { 51 + switch kt { 52 + case P256: 53 + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 54 + if err != nil { 55 + return nil, fmt.Errorf("P-256/secp256r1 key generation failed: %w", err) 56 + } 57 + return &PrivateKey{keyType: kt, privP256: key}, nil 58 + case K256: 59 + key, err := secp256k1secec.GenerateKey() 60 + if err != nil { 61 + return nil, fmt.Errorf("K-256/secp256k1 key generation failed: %w", err) 62 + } 63 + return &PrivateKey{keyType: kt, privK256: key}, nil 64 + default: 65 + return nil, fmt.Errorf("unexpected crypto KeyType") 66 + } 67 + } 68 + 69 + // Loads a [PrivateKey] of the indicated curve type from raw bytes, as exported by the [PrivateKey.Bytes()] method. 70 + // 71 + // Calling code needs to know the key type ahead of time, and must remove any string encoding (hex encoding, base64, etc) before calling this function. 72 + func ParsePrivateKeyBytes(data []byte, kt KeyType) (*PrivateKey, error) { 73 + switch kt { 74 + case P256: 75 + // elaborately parse as an ecdh.PrivateKey, then get from that to ecdsa.PrivateKey by encoding/decoding using x509 PKCS8 encoding. 76 + // Note that the 'data' bytes format is *not* x509 PKCS8! 77 + skEcdh, err := ecdh.P256().NewPrivateKey(data) 78 + if err != nil { 79 + return nil, fmt.Errorf("invalid P-256/secp256r1 private key: %w", err) 80 + } 81 + enc, err := x509.MarshalPKCS8PrivateKey(skEcdh) 82 + if err != nil { 83 + return nil, fmt.Errorf("invalid P-256/secp256r1 private key: %w", err) 84 + } 85 + sk, err := x509.ParsePKCS8PrivateKey(enc) 86 + if err != nil { 87 + return nil, fmt.Errorf("invalid P-256/secp256r1 private key: %w", err) 88 + } 89 + return &PrivateKey{keyType: kt, privP256: sk.(*ecdsa.PrivateKey)}, nil 90 + case K256: 91 + sk, err := secp256k1secec.NewPrivateKey(data) 92 + if err != nil { 93 + return nil, fmt.Errorf("invalid K-256/secp256k1 private key: %w", err) 94 + } 95 + return &PrivateKey{keyType: kt, privK256: sk}, nil 96 + default: 97 + return nil, fmt.Errorf("unexpected crypto KeyType") 98 + } 99 + } 100 + 101 + // Checks if the two private keys are the same. Note that the naive == operator does not work for most equality checks. 102 + func (k *PrivateKey) Equal(other *PrivateKey) bool { 103 + if k.keyType != other.keyType { 104 + return false 105 + } 106 + switch k.keyType { 107 + case P256: 108 + return k.privP256.Equal(other.privP256) 109 + case K256: 110 + return k.privK256.Equal(other.privK256) 111 + default: 112 + panic("unexpected crypto KeyType") 113 + } 114 + } 115 + 116 + func (k *PrivateKey) KeyType() KeyType { 117 + return k.keyType 118 + } 119 + 120 + // Serializes the secret key material in to a raw binary format, which can be parsed by [ParsePrivateKeyBytes]. 121 + // 122 + // The encoding format is curve-specific, and is generally "compact" for private keys. Both P-256 and K-256 private keys end up 32 bytes long. There is no ASN.1 or other enclosing structure to the binary encoding. 123 + func (k *PrivateKey) Bytes() ([]byte, error) { 124 + switch k.keyType { 125 + case P256: 126 + skEcdh, err := k.privP256.ECDH() 127 + if err != nil { 128 + return nil, fmt.Errorf("unexpected failure to convert key type: %w", err) 129 + } 130 + return skEcdh.Bytes(), nil 131 + case K256: 132 + return k.privK256.Bytes(), nil 133 + default: 134 + return nil, fmt.Errorf("unexpected crypto KeyType") 135 + } 136 + } 137 + 138 + // Outputs the PublicKey corresponding to this PrivateKey. 139 + func (k *PrivateKey) Public() PublicKey { 140 + switch k.keyType { 141 + case P256: 142 + return PublicKey{ 143 + keyType: k.keyType, 144 + pubP256: k.privP256.Public().(*ecdsa.PublicKey), 145 + } 146 + case K256: 147 + return PublicKey{ 148 + keyType: k.keyType, 149 + pubK256: k.privK256.PublicKey(), 150 + } 151 + default: 152 + panic("unexpected crypto KeyType") 153 + } 154 + } 155 + 156 + // First hashes the raw bytes, then signs the digest, returning a binary signature. 157 + // 158 + // SHA-256 is the hash algorithm used, as specified by atproto. Signing digests is the norm for ECDSA, and required by some backend implementations. This method does not "double hash", it simply has name which clarifies that hashing is happening. 159 + // 160 + // Calling code is responsible for any string encoding of signatures (eg, hex or base64). Both P-256 and K-256 signatures are 64 bytes long. 161 + // 162 + // NIST ECDSA signatures can have a "malleability" issue, meaning that there are multiple valid signatures for the same content with the same signing key. This method always returns a "low-S" signature, as required by atproto. 163 + func (k *PrivateKey) HashAndSign(content []byte) ([]byte, error) { 164 + hash := sha256.Sum256(content) 165 + switch k.keyType { 166 + case P256: 167 + r, s, err := ecdsa.Sign(rand.Reader, k.privP256, hash[:]) 168 + if err != nil { 169 + return nil, fmt.Errorf("crypto error signing with P-256/secp256r1 private key: %w", err) 170 + } 171 + s = sigSToLowS_P256(s) 172 + sig := make([]byte, 64) 173 + r.FillBytes(sig[:32]) 174 + s.FillBytes(sig[32:]) 175 + return sig, nil 176 + case K256: 177 + return k.privK256.Sign(rand.Reader, hash[:], k256Options) 178 + default: 179 + return nil, fmt.Errorf("unexpected crypto KeyType") 180 + } 181 + } 182 + 183 + // Checks if the two public keys are the same. Note that the naive == operator does not work for most equality checks. 184 + func (k *PublicKey) Equal(other *PublicKey) bool { 185 + if k.keyType != other.keyType { 186 + return false 187 + } 188 + switch k.keyType { 189 + case P256: 190 + return k.pubP256.Equal(other.pubP256) 191 + case K256: 192 + return k.pubK256.Equal(other.pubK256) 193 + default: 194 + panic("unexpected crypto KeyType") 195 + } 196 + } 197 + 198 + // Loads a [PublicKey] of the indicated curve type from raw bytes, as exported by the [PublicKey.CompressedBytes] method. 199 + // 200 + // Calling code needs to know the key type ahead of time, and must remove any string encoding (hex encoding, base64, etc) before calling this function. 201 + func ParsePublicCompressedBytes(data []byte, kt KeyType) (*PublicKey, error) { 202 + switch kt { 203 + case P256: 204 + curve := elliptic.P256() 205 + x, y := elliptic.UnmarshalCompressed(curve, data) 206 + if x == nil { 207 + return nil, fmt.Errorf("invalid P-256 public key (x==nil)") 208 + } 209 + if !curve.Params().IsOnCurve(x, y) { 210 + return nil, fmt.Errorf("invalid P-256 public key (not on curve)") 211 + } 212 + pub := &ecdsa.PublicKey{ 213 + Curve: curve, 214 + X: x, 215 + Y: y, 216 + } 217 + return &PublicKey{ 218 + keyType: kt, 219 + pubP256: pub, 220 + }, nil 221 + case K256: 222 + // secp256k1secec.NewPublicKey accepts any valid encoding, while we 223 + // explicitly want compressed, so use the explicit point 224 + // decompression routine. 225 + p, err := secp256k1.NewIdentityPoint().SetCompressedBytes(data) 226 + if err != nil { 227 + return nil, fmt.Errorf("invalid K-256/secp256k1 public key: %w", err) 228 + } 229 + 230 + pub, err := secp256k1secec.NewPublicKeyFromPoint(p) 231 + if err != nil { 232 + return nil, fmt.Errorf("invalid K-256/secp256k1 public key: %w", err) 233 + } 234 + return &PublicKey{ 235 + keyType: kt, 236 + pubK256: pub, 237 + }, nil 238 + default: 239 + return nil, fmt.Errorf("unexpected crypto KeyType") 240 + } 241 + } 242 + 243 + // Loads a [PublicKey] of the indicated curve type from raw bytes, as exported by the [PublicKey.CompressedBytes] method. 244 + // 245 + // Calling code needs to know the key type ahead of time, and must remove any string encoding (hex encoding, base64, etc) before calling this function. 246 + func ParsePublicUncompressedBytes(data []byte, kt KeyType) (*PublicKey, error) { 247 + switch kt { 248 + case P256: 249 + curve := elliptic.P256() 250 + x, y := elliptic.Unmarshal(curve, data) 251 + if x == nil { 252 + return nil, fmt.Errorf("invalid P-256 public key (x==nil)") 253 + } 254 + if !curve.Params().IsOnCurve(x, y) { 255 + return nil, fmt.Errorf("invalid P-256 public key (not on curve)") 256 + } 257 + pub := &ecdsa.PublicKey{ 258 + Curve: curve, 259 + X: x, 260 + Y: y, 261 + } 262 + return &PublicKey{ 263 + keyType: kt, 264 + pubP256: pub, 265 + }, nil 266 + case K256: 267 + pub, err := secp256k1secec.NewPublicKey(data) 268 + if err != nil { 269 + return nil, fmt.Errorf("invalid K-256/secp256k1 public key: %w", err) 270 + } 271 + return &PublicKey{ 272 + keyType: kt, 273 + pubK256: pub, 274 + }, nil 275 + default: 276 + return nil, fmt.Errorf("unexpected crypto KeyType") 277 + } 278 + } 279 + 280 + // Parses a public key in multibase encoding, as would be found in a DID Document `verificationMethod` section. 281 + // 282 + // This implementation does not handle the many possible multibase encodings (eg, base32), only the base58btc encoding that would be found in a DID Document. 283 + func ParsePublicMultibase(encoded string, kt KeyType) (*PublicKey, error) { 284 + if len(encoded) < 2 || encoded[0] != 'z' { 285 + return nil, fmt.Errorf("crypto: not a multibase base58btc string") 286 + } 287 + data, err := base58.Decode(encoded[1:]) 288 + if err != nil { 289 + return nil, fmt.Errorf("crypto: not a multibase base58btc string") 290 + } 291 + return ParsePublicUncompressedBytes(data, kt) 292 + } 293 + 294 + // Parses a public key in a variant of multibase encoding, with no key type indicator (unlike did:key), but with key compression (unlike `verificationMethod` in a DID Document). 295 + func ParsePublicCompressedMultibase(encoded string, kt KeyType) (*PublicKey, error) { 296 + if len(encoded) < 2 || encoded[0] != 'z' { 297 + return nil, fmt.Errorf("crypto: not a multibase base58btc string") 298 + } 299 + data, err := base58.Decode(encoded[1:]) 300 + if err != nil { 301 + return nil, fmt.Errorf("crypto: not a multibase base58btc string") 302 + } 303 + return ParsePublicCompressedBytes(data, kt) 304 + } 305 + 306 + // Loads a [PublicKey] from did:key string serialization. 307 + // 308 + // The did:key format encodes the key type. 309 + func ParsePublicDidKey(didKey string) (*PublicKey, error) { 310 + if !strings.HasPrefix(didKey, "did:key:z") { 311 + return nil, fmt.Errorf("string is not a DID key: %s", didKey) 312 + } 313 + mb := strings.TrimPrefix(didKey, "did:key:z") 314 + data, err := base58.Decode(mb) 315 + if err != nil || len(data) < 2 { 316 + return nil, fmt.Errorf("crypto: not a multibase base58btc string") 317 + } 318 + if data[0] == 0x80 && data[1] == 0x24 { 319 + // multicodec p256-pub, code 0x1200, varint-encoded bytes: [0x80, 0x24] 320 + return ParsePublicCompressedBytes(data[2:], P256) 321 + } else if data[0] == 0xE7 && data[1] == 0x01 { 322 + // multicodec secp256k1-pub, code 0xE7, varint bytes: [0xE7, 0x01] 323 + return ParsePublicCompressedBytes(data[2:], K256) 324 + } else { 325 + return nil, fmt.Errorf("unexpected did:key multicode value") 326 + } 327 + } 328 + 329 + // Serializes the [PublicKey] in to "uncompressed" binary format. 330 + func (k *PublicKey) UncompressedBytes() []byte { 331 + switch k.keyType { 332 + case P256: 333 + pkEcdh, err := k.pubP256.ECDH() 334 + if err != nil { 335 + panic("unexpected invalid P-256/secp256r1 public key (internal)") 336 + } 337 + return pkEcdh.Bytes() 338 + case K256: 339 + p := k.pubK256.Point() 340 + // NOTE: is this check necessary for uncompressed bytes? came from go-did 341 + if p.IsIdentity() != 0 { 342 + panic("unexpected invalid K-256/secp256k1 public key (internal)") 343 + } 344 + return p.UncompressedBytes() 345 + default: 346 + panic("unexpected crypto KeyType") 347 + } 348 + } 349 + 350 + // Serializes the [PublicKey] in to "compressed" binary format. 351 + func (k *PublicKey) CompressedBytes() []byte { 352 + switch k.keyType { 353 + case P256: 354 + if !k.pubP256.Curve.IsOnCurve(k.pubP256.X, k.pubP256.Y) { 355 + panic("unexpected invalid P-256/secp256r1 public key (internal)") 356 + } 357 + return elliptic.MarshalCompressed(k.pubP256.Curve, k.pubP256.X, k.pubP256.Y) 358 + case K256: 359 + p := k.pubK256.Point() 360 + if p.IsIdentity() != 0 { 361 + panic("unexpected invalid K-256/secp256k1 public key (internal)") 362 + } 363 + return p.CompressedBytes() 364 + default: 365 + panic("unexpected crypto KeyType") 366 + } 367 + } 368 + 369 + // First hashes the raw bytes, then verifies the digest, returning `nil` for valid signatures, or an error for any failure. 370 + // 371 + // SHA-256 is the hash algorithm used, as specified by atproto. Signing digests is the norm for ECDSA, and required by some backend implementations. This method does not "double hash", it simply has name which clarifies that hashing is happening. 372 + // 373 + // Calling code is responsible for any string decoding of signatures (eg, hex or base64) before calling this function. 374 + // 375 + // This method requires a "low-S" signature, as specified by atproto. 376 + func (k *PublicKey) HashAndVerify(content, sig []byte) error { 377 + hash := sha256.Sum256(content) 378 + switch k.keyType { 379 + case P256: 380 + // parseP256Sig 381 + if len(sig) != 64 { 382 + return fmt.Errorf("crypto: P-256 signatures must be 64 bytes, got len=%d", len(sig)) 383 + } 384 + r := big.NewInt(0) 385 + s := big.NewInt(0) 386 + r.SetBytes(sig[:32]) 387 + s.SetBytes(sig[32:]) 388 + 389 + if !ecdsa.Verify(k.pubP256, hash[:], r, s) { 390 + return fmt.Errorf("crypto: invalid signature") 391 + } 392 + 393 + // ensure that signature is low-S 394 + if !sigSIsLowS_P256(s) { 395 + return fmt.Errorf("crypto: invalid signature (high-S P-256)") 396 + } 397 + 398 + return nil 399 + case K256: 400 + if !k.pubK256.Verify(hash[:], sig, k256Options) { 401 + return fmt.Errorf("crypto: invalid signature") 402 + } 403 + return nil 404 + default: 405 + return fmt.Errorf("unexpected crypto KeyType") 406 + } 407 + } 408 + 409 + // Returns a did:key string encoding of the public key, as would be encoded in a DID PLC operation: 410 + // 411 + // - compressed / compacted binary representation 412 + // - prefix with appropriate curve multicodec bytes 413 + // - encode bytes with base58btc 414 + // - add "z" prefix to indicate encoding 415 + // - add "did:key:" prefix 416 + func (k *PublicKey) DidKey() string { 417 + kbytes := k.CompressedBytes() 418 + switch k.keyType { 419 + case P256: 420 + // multicodec p256-pub, code 0x1200, varint-encoded bytes: [0x80, 0x24] 421 + kbytes = append([]byte{0x80, 0x24}, kbytes...) 422 + case K256: 423 + // multicodec secp256k1-pub, code 0xE7, varint bytes: [0xE7, 0x01] 424 + kbytes = append([]byte{0xE7, 0x01}, kbytes...) 425 + default: 426 + panic("unexpected crypto KeyType") 427 + } 428 + return "did:key:z" + base58.Encode(kbytes) 429 + } 430 + 431 + // Returns multibase string encoding of the public key, as would be included in a DID Document "verificationMethod" section: 432 + // 433 + // - non-compressed / non-compacted binary representation 434 + // - encode bytes with base58btc 435 + // - prefix "z" (lower-case) to indicate encoding 436 + func (k *PublicKey) Multibase() string { 437 + kbytes := k.UncompressedBytes() 438 + return "z" + base58.Encode(kbytes) 439 + } 440 + 441 + // Variant of Multibase() which outputs compressed key format. 442 + func (k *PublicKey) CompressedMultibase() string { 443 + kbytes := k.CompressedBytes() 444 + return "z" + base58.Encode(kbytes) 445 + } 446 + 447 + func (k *PublicKey) KeyType() KeyType { 448 + return k.keyType 449 + } 450 + 451 + // Returns the DID cryptographic suite string which would be included in the `type` field of a `verificationMethod`. 452 + func (k *PublicKey) DidDocSuite() string { 453 + switch k.keyType { 454 + case P256: 455 + return "EcdsaSecp256r1VerificationKey2019" 456 + case K256: 457 + // NOTE: this is not a W3C standard suite, and will probably be replaced with "Multikey" 458 + return "EcdsaSecp256k1VerificationKey2019" 459 + default: 460 + panic("unexpected crypto KeyType") 461 + } 462 + }
+137
atproto/crypto/keys_test.go
··· 1 + package crypto 2 + 3 + import ( 4 + "crypto/rand" 5 + "testing" 6 + 7 + "github.com/stretchr/testify/assert" 8 + ) 9 + 10 + var keyTypes = []KeyType{K256, P256} 11 + 12 + func TestKeyBasics(t *testing.T) { 13 + assert := assert.New(t) 14 + 15 + // try signing/verifying a couple different message sizes. these all just get hashed. 16 + msg := []byte("test-message") 17 + midMsg := make([]byte, 13*1024) 18 + _, err := rand.Read(midMsg) 19 + bigMsg := make([]byte, 16*1024*1024) 20 + _, err = rand.Read(bigMsg) 21 + assert.NoError(err) 22 + 23 + for _, kt := range keyTypes { 24 + // private key generation and encoding 25 + priv, err := GeneratePrivateKey(kt) 26 + assert.NoError(err) 27 + privBytes, err := priv.Bytes() 28 + assert.NoError(err) 29 + privFromBytes, err := ParsePrivateKeyBytes(privBytes, kt) 30 + assert.NoError(err) 31 + assert.Equal(priv, privFromBytes) 32 + 33 + // public key encoding 34 + pub := priv.Public() 35 + 36 + pubCompBytes := pub.CompressedBytes() 37 + pubFromCompBytes, err := ParsePublicCompressedBytes(pubCompBytes, kt) 38 + assert.NoError(err) 39 + assert.True(pub.Equal(pubFromCompBytes)) 40 + 41 + pubUncompBytes := pub.UncompressedBytes() 42 + pubFromUncompBytes, err := ParsePublicUncompressedBytes(pubUncompBytes, kt) 43 + assert.NoError(err) 44 + assert.True(pub.Equal(pubFromUncompBytes)) 45 + 46 + pubDidKeyString := pub.DidKey() 47 + pubDK, err := ParsePublicDidKey(pubDidKeyString) 48 + assert.NoError(err) 49 + assert.True(pub.Equal(pubDK)) 50 + 51 + pubMultibaseString := pub.Multibase() 52 + pubMB, err := ParsePublicMultibase(pubMultibaseString, kt) 53 + assert.NoError(err) 54 + assert.True(pub.Equal(pubMB)) 55 + 56 + pubCompMultibaseString := pub.CompressedMultibase() 57 + pubCMB, err := ParsePublicCompressedMultibase(pubCompMultibaseString, kt) 58 + assert.NoError(err) 59 + assert.True(pub.Equal(pubCMB)) 60 + 61 + // signature verification 62 + sig, err := priv.HashAndSign(msg) 63 + assert.NoError(err) 64 + assert.NoError(pub.HashAndVerify(msg, sig)) 65 + 66 + midSig, err := priv.HashAndSign(midMsg) 67 + assert.NoError(err) 68 + assert.NoError(pub.HashAndVerify(midMsg, midSig)) 69 + 70 + bigSig, err := priv.HashAndSign(bigMsg) 71 + assert.NoError(err) 72 + assert.NoError(pub.HashAndVerify(bigMsg, bigSig)) 73 + } 74 + } 75 + 76 + // this does a large number of sign/verify cycles, to try and hit any bad high-S signatures 77 + func TestLowSMany(t *testing.T) { 78 + assert := assert.New(t) 79 + 80 + msg := make([]byte, 1024) 81 + 82 + for _, kt := range keyTypes { 83 + for i := 0; i < 128; i++ { 84 + priv, err := GeneratePrivateKey(kt) 85 + assert.NoError(err) 86 + pub := priv.Public() 87 + 88 + _, err = rand.Read(msg) 89 + assert.NoError(err) 90 + 91 + sig, err := priv.HashAndSign(msg) 92 + assert.NoError(err) 93 + err = pub.HashAndVerify(msg, sig) 94 + assert.NoError(err) 95 + // bail out early instead of looping 96 + if err != nil { 97 + break 98 + } 99 + } 100 + } 101 + } 102 + 103 + func TestKeyCompressionP256(t *testing.T) { 104 + assert := assert.New(t) 105 + 106 + priv, err := GeneratePrivateKey(P256) 107 + assert.NoError(err) 108 + privBytes, err := priv.Bytes() 109 + assert.NoError(err) 110 + pub := priv.Public() 111 + sig, err := priv.HashAndSign([]byte("test-message")) 112 + assert.NoError(err) 113 + 114 + // P-256 key and signature sizes 115 + assert.Equal(32, len(privBytes)) 116 + assert.Equal(33, len(pub.CompressedBytes())) 117 + assert.Equal(65, len(pub.UncompressedBytes())) 118 + assert.Equal(64, len(sig)) 119 + } 120 + 121 + func TestKeyCompressionK256(t *testing.T) { 122 + assert := assert.New(t) 123 + 124 + priv, err := GeneratePrivateKey(K256) 125 + assert.NoError(err) 126 + privBytes, err := priv.Bytes() 127 + assert.NoError(err) 128 + pub := priv.Public() 129 + sig, err := priv.HashAndSign([]byte("test-message")) 130 + assert.NoError(err) 131 + 132 + // K-256 key and signature sizes 133 + assert.Equal(32, len(privBytes)) 134 + assert.Equal(33, len(pub.CompressedBytes())) 135 + assert.Equal(65, len(pub.UncompressedBytes())) 136 + assert.Equal(64, len(sig)) 137 + }
+31
atproto/crypto/p256_lows.go
··· 1 + package crypto 2 + 3 + import ( 4 + "crypto/elliptic" 5 + "math/big" 6 + ) 7 + 8 + var curveN_P256 *big.Int = elliptic.P256().Params().N 9 + var curveHalfOrder_P256 *big.Int = new(big.Int).Rsh(curveN_P256, 1) 10 + 11 + // Checks if 'S' value from a P-256 signature is "low-S". 12 + // un-reviewed, un-safe code from: https://github.com/golang/go/issues/54549 13 + func sigSIsLowS_P256(s *big.Int) bool { 14 + if s.Cmp(curveHalfOrder_P256) == 1 { 15 + return false 16 + } 17 + return true 18 + } 19 + 20 + // Ensures that 'S' value from a P-256 signature is "low-S" variant. 21 + // un-reviewed, un-safe code from: https://github.com/golang/go/issues/54549 22 + func sigSToLowS_P256(s *big.Int) *big.Int { 23 + 24 + if !sigSIsLowS_P256(s) { 25 + // Set s to N - s that will be then in the lower part of signature space 26 + // less or equal to half order 27 + s.Sub(curveN_P256, s) 28 + return s 29 + } 30 + return s 31 + }
+38
atproto/crypto/testdata/signature-fixtures.json
··· 1 + [ 2 + { 3 + "messageBase64": "oWVoZWxsb2V3b3JsZA", 4 + "algorithm": "ES256", 5 + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", 6 + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", 7 + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", 8 + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg", 9 + "validSignature": true 10 + }, 11 + { 12 + "messageBase64": "oWVoZWxsb2V3b3JsZA", 13 + "algorithm": "ES256K", 14 + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", 15 + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", 16 + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", 17 + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA", 18 + "validSignature": true 19 + }, 20 + { 21 + "messageBase64": "oWVoZWxsb2V3b3JsZA", 22 + "algorithm": "ES256", 23 + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", 24 + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", 25 + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", 26 + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw", 27 + "validSignature": false 28 + }, 29 + { 30 + "messageBase64": "oWVoZWxsb2V3b3JsZA", 31 + "algorithm": "ES256K", 32 + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", 33 + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", 34 + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", 35 + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ", 36 + "validSignature": false 37 + } 38 + ]
+22
atproto/crypto/testdata/w3c_didkey_K256.json
··· 1 + [ 2 + { 3 + "privateKeyBytesHex": "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c", 4 + "publicDidKey": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme" 5 + }, 6 + { 7 + "privateKeyBytesHex": "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed", 8 + "publicDidKey": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2" 9 + }, 10 + { 11 + "privateKeyBytesHex": "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02", 12 + "publicDidKey": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N" 13 + }, 14 + { 15 + "privateKeyBytesHex": "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15", 16 + "publicDidKey": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy" 17 + }, 18 + { 19 + "privateKeyBytesHex": "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133", 20 + "publicDidKey": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj" 21 + } 22 + ]
+6
atproto/crypto/testdata/w3c_didkey_P256.json
··· 1 + [ 2 + { 3 + "privateKeyBytesBase58": "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp", 4 + "publicDidKey": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb" 5 + } 6 + ]
+85
atproto/crypto/w3c_didkey_test.go
··· 1 + package crypto 2 + 3 + import ( 4 + "encoding/hex" 5 + "encoding/json" 6 + "io" 7 + "os" 8 + "testing" 9 + 10 + "github.com/mr-tron/base58" 11 + "github.com/stretchr/testify/assert" 12 + ) 13 + 14 + type DidKeyFixture struct { 15 + PrivateKeyBytesBase58 string `json:"privateKeyBytesBase58"` 16 + PrivateKeyBytesHex string `json:"privateKeyBytesHex"` 17 + PublicDidKey string `json:"publicDidKey"` 18 + } 19 + 20 + func TestDidKeyFixtures(t *testing.T) { 21 + 22 + fixtureBatches := []struct { 23 + path string 24 + kt KeyType 25 + }{ 26 + {path: "testdata/w3c_didkey_P256.json", kt: P256}, 27 + {path: "testdata/w3c_didkey_K256.json", kt: K256}, 28 + } 29 + 30 + for _, batch := range fixtureBatches { 31 + 32 + f, err := os.Open(batch.path) 33 + if err != nil { 34 + t.Fatal(err) 35 + } 36 + defer f.Close() 37 + 38 + fixBytes, err := io.ReadAll(f) 39 + if err != nil { 40 + t.Fatal(err) 41 + } 42 + 43 + var fixtures []DidKeyFixture 44 + if err := json.Unmarshal(fixBytes, &fixtures); err != nil { 45 + t.Fatal(err) 46 + } 47 + 48 + for _, row := range fixtures { 49 + testDidKeyFixture(t, row, batch.kt) 50 + } 51 + } 52 + } 53 + 54 + func testDidKeyFixture(t *testing.T, row DidKeyFixture, kt KeyType) { 55 + assert := assert.New(t) 56 + 57 + var raw []byte 58 + var err error 59 + if row.PrivateKeyBytesBase58 != "" { 60 + raw, err = base58.Decode(row.PrivateKeyBytesBase58) 61 + if err != nil { 62 + t.Fatal(err) 63 + } 64 + } else if row.PrivateKeyBytesHex != "" { 65 + raw, err = hex.DecodeString(row.PrivateKeyBytesHex) 66 + if err != nil { 67 + t.Fatal(err) 68 + } 69 + } else { 70 + t.Fatal("no private key found") 71 + } 72 + 73 + priv, err := ParsePrivateKeyBytes(raw, kt) 74 + if err != nil { 75 + t.Fatal(err) 76 + } 77 + kBytes := priv.Public() 78 + kDidKey, err := ParsePublicDidKey(row.PublicDidKey) 79 + if err != nil { 80 + t.Fatal(err) 81 + } 82 + assert.Equal(kBytes.Equal(kDidKey), true) 83 + assert.Equal(row.PublicDidKey, kBytes.DidKey()) 84 + assert.Equal(row.PublicDidKey, kDidKey.DidKey()) 85 + }