keystore

package
v0.0.3-alpha.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2024 License: Apache-2.0 Imports: 22 Imported by: 2

Documentation

Index

Constants

View Source
const (
	DEFAULT_PASSWORD = "123456"

	// Key store file backend sub directories
	PARTITION_ROOT            Partition = ""
	PARTITION_TLS             Partition = "issued"
	PARTITION_ENCRYPTION_KEYS Partition = "encryption-keys"
	PARTITION_SIGNING_KEYS    Partition = "signing-keys"
	PARTITION_HMAC            Partition = "hmac-keys"
	PARTITION_SECRETS         Partition = "secrets"

	// Key store file extensions
	FSEXT_BLOB              FSExtension = ""
	FSEXT_PRIVATE_PKCS8     FSExtension = ".key"
	FSEXT_PRIVATE_PKCS8_PEM FSExtension = ".key.pem"
	FSEXT_PUBLIC_PKCS1      FSExtension = ".pub"
	FSEXT_PUBLIC_PEM        FSExtension = ".pub.pem"
	FSEXT_PRIVATE_BLOB      FSExtension = ".key.bin"
	FSEXT_PUBLIC_BLOB       FSExtension = ".pub.bin"
	FSEXT_DIGEST            FSExtension = ".digest"
	FSEXT_SIG               FSExtension = ".sig"

	// String literals using in platform configuration file
	STORE_PKCS8   StoreType = "pkcs8"
	STORE_PKCS11  StoreType = "pkcs11"
	STORE_TPM2    StoreType = "tpm2"
	STORE_UNKNOWN StoreType = "unknown"

	KEYHALF_PRIVATE KeyHalf = 1 + iota
	KEYHALF_PUBLIC

	// Internal types not stored in the platform configuration file
	KEY_TYPE_ATTESTATION KeyType = 1 + iota
	KEY_TYPE_CA
	KEY_TYPE_ENCRYPTION
	KEY_TYPE_ENDORSEMENT
	KEY_TYPE_HMAC
	KEY_TYPE_IDEVID
	KEY_TYPE_LDEVID
	KEY_TYPE_SECRET
	KEY_TYPE_SIGNING
	KEY_TYPE_STORAGE
	KEY_TYPE_TLS
	KEY_TYPE_TPM

	ENCRYPT_ALGORITHM_RSA_PKCS1v15 = 1 + iota
	ENCRYPT_ALGORITHM_RSA_OAEP
	ENCRYPT_ALGORITHM_ECDH

	WRAP_RSA_OAEP_3072_SHA1_AES_256 WrapAlgorithm = 1 + iota
	WRAP_RSA_OAEP_4096_SHA1_AES_256
	WRAP_RSA_OAEP_3072_SHA256_AES_256
	WRAP_RSA_OAEP_4096_SHA256_AES_256
	WRAP_RSA_OAEP_3072_SHA256
	WRAP_RSA_OAEP_4096_SHA256
	WRAP_AES_GCM
)

Variables

View Source
var (
	ErrFileAlreadyExists = errors.New("store/keystore: file already exists")
	ErrFileNotFound      = errors.New("store/keystore: file not found")

	Partitions = []Partition{
		PARTITION_ROOT,
		PARTITION_TLS,
		PARTITION_HMAC,
		PARTITION_SECRETS,
		PARTITION_ENCRYPTION_KEYS,
		PARTITION_SIGNING_KEYS,
	}
)
View Source
var (
	TemplateRSA = &KeyAttributes{
		CN:           "www.example.com",
		Hash:         crypto.SHA256,
		KeyAlgorithm: x509.RSA,
		KeyType:      KEY_TYPE_TLS,
		Password:     nil,
		RSAAttributes: &RSAAttributes{
			KeySize: 2048,
		},
		SignatureAlgorithm: x509.SHA256WithRSAPSS,
		StoreType:          STORE_PKCS8,
	}

	TemplateECDSA = &KeyAttributes{
		CN: "www.example.com",
		ECCAttributes: &ECCAttributes{
			Curve: elliptic.P256(),
		},
		Hash:               crypto.SHA256,
		KeyAlgorithm:       x509.ECDSA,
		KeyType:            KEY_TYPE_TLS,
		Password:           nil,
		SignatureAlgorithm: x509.ECDSAWithSHA256,
		StoreType:          STORE_PKCS8,
	}

	TemplateEd25519 = &KeyAttributes{
		CN: "www.example.com",
		ECCAttributes: &ECCAttributes{
			Curve: elliptic.P256(),
		},
		Hash:               crypto.SHA256,
		KeyAlgorithm:       x509.Ed25519,
		KeyType:            KEY_TYPE_TLS,
		Password:           nil,
		SignatureAlgorithm: x509.PureEd25519,
		StoreType:          STORE_PKCS8,
	}

	Templates = map[x509.PublicKeyAlgorithm]*KeyAttributes{
		x509.RSA:     TemplateRSA,
		x509.ECDSA:   TemplateECDSA,
		x509.Ed25519: TemplateEd25519,
	}
)
View Source
var (
	ErrAlreadyInitialized        = errors.New("store/keystore: already initialized")
	ErrNotInitalized             = errors.New("store/keystore: not initialized")
	ErrInvalidKeyedHashSecret    = errors.New("store/keystore: invalid keyed hash secret")
	ErrInvalidKeyType            = errors.New("store/keystore: invalid key type")
	ErrInvalidKeyAlgorithm       = errors.New("store/keystore: invalid key algorithm")
	ErrInvalidParentAttributes   = errors.New("store/keystore: invalid parent key attributes")
	ErrInvalidPrivateKey         = errors.New("store/keystore: invalid private key")
	ErrInvalidPrivateKeyRSA      = errors.New("store/keystore: invalid RSA private key")
	ErrInvalidPrivateKeyECDSA    = errors.New("store/keystore: invalid ECDSA private key")
	ErrInvalidPrivateKeyEd25519  = errors.New("store/keystore: invalid Ed25519 private key")
	ErrInvalidOpaquePrivateKey   = errors.New("store/keystore: invalid opaque private key")
	ErrInvalidSignerOpts         = errors.New("store/keystore: invalid signer opts, password required")
	ErrInvalidKeyStore           = errors.New("store/keystore: invalid key store")
	ErrInvalidSignatureAlgorithm = errors.New("store/keystore: unsupported signing algorithm")
	ErrInvalidSignatureScheme    = errors.New("store/keystore: invalid signature scheme")
	ErrKeyAlreadyExists          = errors.New("store/keystore: key already exists")
	ErrFileIntegrityCheckFailed  = errors.New("store/keystore: file integrity check failed")
	ErrInvalidPublicKeyRSA       = errors.New("store/keystore: invalid RSA public key")
	ErrInvalidPublicKeyECDSA     = errors.New("store/keystore: invalid ECDSA public key")
	ErrSingatureVerification     = errors.New("store/keystore: signature verification failed")
	ErrInvalidCurve              = errors.New("store/keystore: invalid ECC curve")
	ErrInvalidHashFunction       = errors.New("store/keystore: invalid hash function")
	ErrInvalidKeyAttributes      = errors.New("store/keystore: invalid key attributes")
	ErrInvalidRSAAttributes      = errors.New("store/keystore: invalid RSA key attributes")
	ErrInvalidECCAttributes      = errors.New("store/keystore: invalid ECC key attributes")
	ErrInvalidBlobName           = errors.New("store/keystore: invalid blob name")
	ErrIssuerAttributeRequired   = errors.New("store/keystore: issuer common name attribute required")
	ErrSOPinRequired             = errors.New("store/keystore: security officer PIN required")
	ErrUserPinRequired           = errors.New("store/keystore: user PIN required")
	ErrInvalidPassword           = errors.New("store/keystore: invalid password")
	ErrInvalidKeyPartition       = errors.New("store/keystore: invalid key partition")
	ErrInvalidEncodingPEM        = errors.New("store/keystore: invalid PEM encoding")
	ErrUnsupportedKeyAlgorithm   = errors.New("store/keystore: unsupported key algorithm")
	ErrPasswordRequired          = errors.New("store/keystore: password required")
)

Functions

func AvailableHashes

func AvailableHashes() map[string]crypto.Hash

Returns a map of crypto.Hash supported by the platform

func AvailableKeyAlgorithms

func AvailableKeyAlgorithms() map[string]x509.PublicKeyAlgorithm

Returns a map of available key algorithms supported by the platform

func AvailableSignatureAlgorithms

func AvailableSignatureAlgorithms() map[string]x509.SignatureAlgorithm

Returns a map of available signature algorithms supported by the platform

func DebugAvailableHashes

func DebugAvailableHashes(logger *logging.Logger)

func DebugAvailableKeyAkgorithms

func DebugAvailableKeyAkgorithms(logger *logging.Logger)

func DebugAvailableSignatureAkgorithms

func DebugAvailableSignatureAkgorithms(logger *logging.Logger)

func DebugKeyAttributes

func DebugKeyAttributes(logger *logging.Logger, attrs *KeyAttributes)

func DecodePrivKeyPEM

func DecodePrivKeyPEM(bytes []byte, password Password) (crypto.PrivateKey, error)

Decodes PEM encoded bytes to crypto.PrivateKey

func DecodePubKeyPEM

func DecodePubKeyPEM(bytes []byte) (crypto.PublicKey, error)

Decodes and returns an ASN.1 DER - PEM encoded - RSA Public Key

func Digest

func Digest(hash crypto.Hash, data []byte) ([]byte, error)

Creates a new digest using the specified hash function

func EncodePEM

func EncodePEM(der []byte) ([]byte, error)

Encodes an ASN.1 DER form public key to PEM form

func EncodePrivKey

func EncodePrivKey(privateKey crypto.PrivateKey, password []byte) ([]byte, error)

Encodes a private key to ASN.1 DER PKCS #8 form

func EncodePrivKeyPEM

func EncodePrivKeyPEM(attrs *KeyAttributes, priv crypto.PrivateKey) ([]byte, error)

Encodes a DER ASN.1 private key to PEM form https://github.com/openssl/openssl/blob/master/include/openssl/pem.h

func EncodePubKey

func EncodePubKey(pub any) ([]byte, error)

Encodes any public key (RSA/ECC) to ASN.1 DER form

func EncodePubKeyPEM

func EncodePubKeyPEM(pub crypto.PublicKey) ([]byte, error)

Encodes a public key to PEM form

func FSEXTKeyAlgorithm

func FSEXTKeyAlgorithm(algo x509.PublicKeyAlgorithm) string

Converts an x509.PublicKeyAlgorithm to a platform file extension

func FSHashName

func FSHashName(hash crypto.Hash) string

Converts crypto.Hash to platform file extension prefix

func FSKeyExtension

func FSKeyExtension(attrs KeyAttributes, ext FSExtension) string

Converts keystore.KeyAttributes to a platform file extension

func HashFileExtension

func HashFileExtension(hash crypto.Hash) string

Converts a hash function name to a file extension. Used to save a signed digest file with an appropriate file extension to the blob

func IsECDSA

func IsECDSA(sigAlgo x509.SignatureAlgorithm) bool

func IsRSAPSS

func IsRSAPSS(sigAlgo x509.SignatureAlgorithm) bool

Returns true if the signature algorithm is one of RSA PSS

func KeyAlgorithmFromSignatureAlgorithm

func KeyAlgorithmFromSignatureAlgorithm(
	sigAlgo x509.SignatureAlgorithm) (x509.PublicKeyAlgorithm, error)

func ParseCurve

func ParseCurve(curve string) (elliptic.Curve, error)

func ParseHash

func ParseHash(hash string) (crypto.Hash, error)

func ParseHashFromSignatureAlgorithm

func ParseHashFromSignatureAlgorithm(algo *x509.SignatureAlgorithm) (crypto.Hash, error)

func ParseKeyAlgorithm

func ParseKeyAlgorithm(algorithm string) (x509.PublicKeyAlgorithm, error)

func ParseKeyAlgorithmFromSignatureAlgorithm

func ParseKeyAlgorithmFromSignatureAlgorithm(
	sigAlgo x509.SignatureAlgorithm) (x509.PublicKeyAlgorithm, error)

func ParseSignatureAlgorithm

func ParseSignatureAlgorithm(algorithm string) (x509.SignatureAlgorithm, error)

func PublicKeyToString

func PublicKeyToString(pub crypto.PublicKey) string

func SecretFromShares

func SecretFromShares(shares []string) (string, error)

Returns secret combined from shares split using Shamir's Secret Sharing algorithm

func ShareSecret

func ShareSecret(required int, secret []byte, shares int) ([]string, error)

Returns a secret split into shares using the Shamir Secret Sharing algorithm. Required is the number of shares required to re-create the secret. Shares is the number of shards the secret is split into.

func SignatureAlgorithmHashes

func SignatureAlgorithmHashes() map[x509.SignatureAlgorithm]crypto.Hash

Types

type ClearPassword

type ClearPassword struct {
	Password
	// contains filtered or unexported fields
}

func (ClearPassword) Bytes

func (p ClearPassword) Bytes() ([]byte, error)

Returns the password as bytes

func (ClearPassword) String

func (p ClearPassword) String() (string, error)

Returns the password as a string

type DecrypterOpts

type DecrypterOpts struct {
	EncryptAttributes  *KeyAttributes
	BlobCN             *string
	BlobData           []byte
	StoreEncryptedBlob bool
	Label              []byte
}

func NewDecrypterOpts

func NewDecrypterOpts() DecrypterOpts

type ECCAttributes

type ECCAttributes struct {
	Curve elliptic.Curve
}

type ECCConfig

type ECCConfig struct {
	Curve string `yaml:"curve" json:"curve" mapstructure:"curve"`
}

type EncryptionAlgorithm

type EncryptionAlgorithm uint8

func (EncryptionAlgorithm) String

func (algo EncryptionAlgorithm) String() string

type FSExtension

type FSExtension string

func KeyFileExtension

func KeyFileExtension(
	algo x509.PublicKeyAlgorithm,
	extension FSExtension,
	keyType *KeyType) FSExtension

Prefixes a key algorithm name to file extension

type FileBackend

type FileBackend struct {
	KeyBackend
	// contains filtered or unexported fields
}

func (*FileBackend) Delete

func (fb *FileBackend) Delete(attrs *KeyAttributes) error

Retrieves the requested data

func (*FileBackend) Get

func (fb *FileBackend) Get(
	attrs *KeyAttributes,
	extension FSExtension) ([]byte, error)

Retrieves the requested data

func (*FileBackend) Save

func (fb *FileBackend) Save(
	attrs *KeyAttributes,
	data []byte,
	extension FSExtension) error

Saves the provided data to the file system

type KeyAttributes

type KeyAttributes struct {
	Debug              bool
	ECCAttributes      *ECCAttributes
	CN                 string
	Hash               crypto.Hash
	KeyAlgorithm       x509.PublicKeyAlgorithm
	KeyType            KeyType
	Parent             *KeyAttributes
	Password           Password
	PlatformPolicy     bool
	RSAAttributes      *RSAAttributes
	Secret             Password
	SignatureAlgorithm x509.SignatureAlgorithm
	StoreType          StoreType
	TPMAttributes      *TPMAttributes
	WrapAttributes     *KeyAttributes
}

func KeyAttributesFromConfig

func KeyAttributesFromConfig(config *KeyConfig) (*KeyAttributes, error)

Parses the provided KeyConfig and returns KeyAttributes

func NewTemplate

func NewTemplate() (*KeyAttributes, error)

Returns a new key attributes template defaulted to RSA 2048

func Template

func Template(algorithm x509.PublicKeyAlgorithm) (*KeyAttributes, error)

Return a template for the given key algorithm

Supported Algorithms: - RSA - ECDSA - Ed25519

Returns ErrInvalidKeyAlgorithm or ErrInvalidKeyType for respective errors

func TemplateFromString

func TemplateFromString(algorithm string) (*KeyAttributes, error)

Parses the provided algorithm and returns the requested template or ErrInvalidKeyAlgorithm if the algorithm is not RSA, ECDSA, or Ed25519.

func (KeyAttributes) String

func (attrs KeyAttributes) String() string

type KeyBackend

type KeyBackend interface {
	Get(attrs *KeyAttributes, extension FSExtension) ([]byte, error)
	Save(attrs *KeyAttributes, data []byte, extension FSExtension) error
	Delete(attrs *KeyAttributes) error
}

func NewFileBackend

func NewFileBackend(
	logger *logging.Logger,
	fs afero.Fs,
	rootDir string) KeyBackend

type KeyConfig

type KeyConfig struct {
	Debug              bool       `json:"debug" yaml:"debug" mapstructure:"debug"`
	ECCConfig          *ECCConfig `yaml:"ecc" json:"ecc" mapstructure:"ecc"`
	CN                 string     `yaml:"cn" json:"cn" mapstructure:"cn"`
	Default            bool       `yaml:"default" json:"default" mapstructure:"default"`
	Hash               string     `yaml:"hash" json:"hash" mapstructure:"hash"`
	KeyAlgorithm       string     `yaml:"algorithm" json:"algorithm" mapstructure:"algorithm"`
	Parent             *KeyConfig `yaml:"parent" json:"parent" mapstructure:"parent"`
	Password           string     `yaml:"password" json:"password" mapstructure:"password"`
	RSAConfig          *RSAConfig `yaml:"rsa" json:"rsa" mapstructure:"rsa"`
	PlatformPolicy     bool       `yaml:"platform-policy" json:"platform-policy" mapstructure:"platform-policy"`
	Secret             string     `yaml:"secret" json:"secret" mapstructure:"secret"`
	SignatureAlgorithm string     `yaml:"signature-algorithm" json:"signature-algorithm" mapstructure:"signature-algorithm"`
	StoreType          string     `yaml:"store" json:"store" mapstructure:"store"`
}

type KeyHalf

type KeyHalf uint8

type KeyStorer

type KeyStorer interface {
	Backend() KeyBackend
	Close() error
	Delete(attrs *KeyAttributes) error
	GenerateKey(attrs *KeyAttributes) (OpaqueKey, error)
	GenerateRSA(attrs *KeyAttributes) (OpaqueKey, error)
	GenerateECDSA(attrs *KeyAttributes) (OpaqueKey, error)
	GenerateEd25519(attrs *KeyAttributes) (OpaqueKey, error)
	GenerateSecretKey(attrs *KeyAttributes) error
	Decrypter(attrs *KeyAttributes) (crypto.Decrypter, error)
	Equal(opaque Opaque, x crypto.PrivateKey) bool
	Initialize(soPIN, userPIN Password) error
	Key(attrs *KeyAttributes) (OpaqueKey, error)
	Signer(attrs *KeyAttributes) (crypto.Signer, error)
	Type() StoreType
	Verifier(attrs *KeyAttributes, opts *VerifyOpts) Verifier
}

The Key Store Module interface

type KeyType

type KeyType uint8

func (KeyType) String

func (keyType KeyType) String() string

type Opaque

type Opaque struct {
	// contains filtered or unexported fields
}

func (*Opaque) Decrypt

func (opaque *Opaque) Decrypt(
	rand io.Reader,
	msg []byte,
	opts crypto.DecrypterOpts) (plaintext []byte, err error)

Implements crypto.Decrypter https://pkg.go.dev/crypto#Decrypter

func (*Opaque) Digest

func (opaque *Opaque) Digest(data []byte) ([]byte, error)

Creates a digest using the hash function defined by the key's signing attributes

func (*Opaque) Equal

func (opaque *Opaque) Equal(x crypto.PrivateKey) bool

Implements crypto.PrivateKey https://pkg.go.dev/crypto#PrivateKey

func (*Opaque) KeyAttributes

func (opaque *Opaque) KeyAttributes() *KeyAttributes

Returns the opaque key's attributes

func (*Opaque) Public

func (opaque *Opaque) Public() crypto.PublicKey

Returns the public half of the opaque key Implements crypto.Signer https://pkg.go.dev/crypto#Signer

func (*Opaque) Sign

func (opaque *Opaque) Sign(
	rand io.Reader,
	digest []byte,
	opts crypto.SignerOpts) (signature []byte, err error)

Implements crypto.Signer https://pkg.go.dev/crypto#Signer

type OpaqueKey

type OpaqueKey interface {
	Digest(data []byte) ([]byte, error)
	KeyAttributes() *KeyAttributes
	crypto.PrivateKey
	crypto.Signer
	crypto.Decrypter
}

func NewOpaqueKey

func NewOpaqueKey(
	keyStore KeyStorer,
	attrs *KeyAttributes,
	pub crypto.PublicKey) OpaqueKey

Create an opaque private key backed by the underlying key store

type Partition

type Partition string

type Password

type Password interface {
	String() (string, error)
	Bytes() ([]byte, error)
}

Passwords are a type of secret that are often used to access computer systems or enter a place, such as a protected vault.

func NewClearPassword

func NewClearPassword(password []byte) Password

Creates a new clear text password stored in memory

func NewClearPasswordFromString

func NewClearPasswordFromString(password string) Password

Creates a new clear text password stored in memory from a string

type RSAAttributes

type RSAAttributes struct {
	KeySize int
}

type RSAConfig

type RSAConfig struct {
	KeySize int `yaml:"size" json:"size" mapstructure:"size"`
}

type SignerOpts

type SignerOpts struct {
	Backend       KeyBackend
	KeyAttributes *KeyAttributes

	// Optional PSS Salt Length when using RSA PSS.
	// Default rsa.PSSSaltLengthEqualsHash
	PSSOptions *rsa.PSSOptions
	BlobCN     []byte
	BlobData   []byte

	crypto.SignerOpts
	// contains filtered or unexported fields
}

func NewSignerOpts

func NewSignerOpts(
	attrs *KeyAttributes,
	data []byte) *SignerOpts

func (SignerOpts) Digest

func (opts SignerOpts) Digest() ([]byte, error)

func (SignerOpts) HashFunc

func (opts SignerOpts) HashFunc() crypto.Hash

type SignerStore

type SignerStore struct {
	SignerStorer
	// contains filtered or unexported fields
}

func (SignerStore) Checksum

func (ks SignerStore) Checksum(verifyOpts *VerifyOpts) ([]byte, error)

Returns a stored checksum from the signed blob store

func (SignerStore) SaveSignature

func (ks SignerStore) SaveSignature(
	opts any,
	signature, digest []byte) error

Signs and saves blobs passed to a signer as SignerOpts

type SignerStorer

type SignerStorer interface {
	Checksum(verifyOpts *VerifyOpts) ([]byte, error)
	SaveSignature(opts any, signature, digest []byte) error
}

func NewSignerStore

func NewSignerStore(
	blobStore blobstore.BlobStorer) SignerStorer

Provides blob storage for signed data

type StoreType

type StoreType string

func ParseStoreType

func ParseStoreType(storeType string) (StoreType, error)

func (StoreType) String

func (st StoreType) String() string

type TPMAttributes

type TPMAttributes struct {
	BPublic              tpm2.TPM2BPublic
	CertHandle           tpm2.TPMHandle
	CertifyInfo          []byte
	CreationTicketDigest []byte
	Handle               tpm2.TPMHandle
	HandleType           tpm2.TPMHT
	HashAlg              tpm2.TPMIAlgHash
	Hierarchy            tpm2.TPMHandle
	HierarchyAuth        Password
	Name                 tpm2.TPM2BName
	PCRSelection         tpm2.TPMLPCRSelection
	PublicKeyBytes       []byte
	Public               tpm2.TPMTPublic
	PrivateKeyBlob       []byte
	SessionCloser        func() error
	Signature            []byte
	Template             tpm2.TPMTPublic
}

type Verifier

type Verifier interface {
	Verify(
		pub crypto.PublicKey,
		hash crypto.Hash,
		hashed, signature []byte,
		opts *VerifyOpts) error
}

func NewVerifier

func NewVerifier(signerStore SignerStorer) Verifier

Verifies a signature

type Verify

type Verify struct {
	Verifier
	// contains filtered or unexported fields
}

func (*Verify) Verify

func (verifier *Verify) Verify(
	pub crypto.PublicKey,
	hash crypto.Hash,
	hashed, signature []byte,
	opts *VerifyOpts) error

Verifies a RSA (PKCS1v15 or PSS) or ECDSA digest using the supplied public key, hash function, and signature. Optional verification opts may be passed during instantiation to perform custom verification using key attributes and file integrity checking using the sum from a previously captured signing operation. Optional *rsa.PSSOptions may also be provided via the verifier opts to perform the verification using the RSA-PSS padding scheme. If not, PKCS1v15 will be used as the default.

type VerifyOpts

type VerifyOpts struct {
	KeyAttributes  *KeyAttributes
	BlobCN         []byte
	IntegrityCheck bool
	PSSOptions     *rsa.PSSOptions
}

type WrapAlgorithm

type WrapAlgorithm uint8

func (WrapAlgorithm) String

func (algo WrapAlgorithm) String() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL